CodeIgniter and Doctrine From Scratch
CodeIgniter and Doctrine From Scratch
• Go to http://localhost/ci_doctrine_day1
You should see this:
CodeIgniter Crash Course: Controllers
Controllers are called by CodeIgniter on every page load.
They are located under:
system/application/controllers/
The url structure looks like this:
http://localhost/ci_doctrine_day1/index.php/CONTROLLER_NAME/FUNCTION_NAME
For example if you open this url:
http://localhost/ci_doctrine_day1/index.php/hello/world
CodeIgniter will look for a controller class named “Hello” and call it’s method named
“world()”.
So let’s create our first controller.
Our First Controller
• Create this file: system/application/controllers/hello.php
view source
print?
01 <?php
02 // system/application/controllers/hello.php
03
04 class Hello extends Controller {
05
06 function world() {
05 require_once APPPATH.'/plugins/doctrine/lib/Doctrine.php';
06
09
10 // this will allow Doctrine to load Model classes automatically
11 spl_autoload_register(array('Doctrine', 'autoload'));
12
19 '://' . $db[$connection_name]['username'] .
20 ':' . $db[$connection_name]['password'].
21 '@' . $db[$connection_name]['hostname'] .
22 '/' . $db[$connection_name]['database'];
23
24 Doctrine_Manager::connection($dsn,$connection_name);
25 }
26
27 // CodeIgniter's Model class needs to be loaded
28 require_once BASEPATH.'/libraries/Model.php';
29
30 // telling Doctrine where our models are located
31 Doctrine::loadModels(APPPATH.'/models');
32
37 Doctrine::ATTR_AUTO_ACCESSOR_OVERRIDE, true);
38
// this sets all table columns to notnull and unsigned (for ints) by
39
default
40 Doctrine_Manager::getInstance()->setAttribute(
41 Doctrine::ATTR_DEFAULT_COLUMN_OPTIONS,
42 array('notnull' => true, 'unsigned' => true));
43
44 // set the default primary key to be named 'id', integer, 4 bytes
45 Doctrine_Manager::getInstance()->setAttribute(
46 Doctrine::ATTR_DEFAULT_IDENTIFIER_OPTIONS,
3
4 $db['default']['hostname'] = "localhost";
5 $db['default']['username'] = "root";
6 $db['default']['password'] = "";
7 $db['default']['database'] = "ci_doctrine";
8
9 // ...
We just edited the database configuration file of CodeIgniter.
More Configuration
Almost done.
config.php
• Edit file: system/application/config/config.php
view source
print?
1 // in system/application/config/config.php
2 // ...
3
4 $config['base_url'] = "http://localhost/ci_doctrine_day1/";
5
6 // ...
Now CodeIgniter knows the url of our site.
autoload.php
• Edit file: system/application/config/autoload.php
view source
print?
1 // in system/application/config/autoload.php
2 // ...
3
4 $autoload['plugin'] = array('doctrine');
5
6 // ...
This makes sure the Doctrine plug-in is always loaded.
Finished!
Now we’re ready to rock. Let’s start testing our setup.
Our First Doctrine Model
Create a user Table
• Open phpMyAdmin: http://localhost/phpmyadmin/
• Go to database “ci_doctrine”
• Create a table named “user” with columns:
id => int, primary key, auto_increment,
username => varchar(255), unique,
password => varchar(255),
first_name => varchar(255),
last_name => varchar(255)
You may use this query:
CREATE TABLE `ci_doctrine`.`user` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`username` VARCHAR( 255 ) NOT NULL ,
`password` VARCHAR( 255 ) NOT NULL ,
`first_name` VARCHAR( 255 ) NOT NULL ,
`last_name` VARCHAR( 255 ) NOT NULL ,
UNIQUE (
`username`
)
)
11
12 }
Note:
• We extend Doctrine_Record, instead of Model (which you normally would
with CodeIgniter models).
• Inside the function setTableDefinition() we need to define the table structure.
• By default, Doctrine will look for a table with same name as the class. In this
case: “user”. (this can be changed)
• In our doctrine_pi.php file above in this tutorial, we specified for a default
primary key named “id”. Therefore we don’t need to put it again in our User
class.
Testing the Model: Add Some Users
• Edit our controller we created earlier:
system/application/controllers/hello.php
view source
print?
01 <?php
02 // system/application/controllers/hello.php
03
04 class Hello extends Controller {
05
06 function world() {
09
10 function user_test() {
11
12 $u = new User;
13 $u->username = 'johndoe';
14 $u->password = 'secret';
15 $u->first_name = 'John';
16 $u->last_name = 'Doe';
17 $u->save();
18
21 $u2->password = 'mypass';
22 $u2->first_name = 'Codeigniter';
23 $u2->last_name = 'Doctrine';
24 $u2->save();
25
26 echo "added 2 users";
27 }
28
29 }
We just generated 2 objects, and populated them with some data. Simply calling save() should
save them into our database.
Note:
• We are able to access the fields as parameters (e.g. $u->username), even
though we did not create these as class parameters. Isn’t Doctrine nice?
• If you are familiar with CodeIgniter, you might remember that you need to
call $this->load->model() function to load models. However since we
registered the autoload function of Doctrine, just saying “new User;” is
enough.
• We didn’t create the “save()” function, because it comes from the
Doctrine_Record class we extended. It saves the objects to the database.
There are many other functions and goodies that come with Doctrine classes,
we will see later in the tutorials.
• Open: http://localhost/ci_doctrine_day1/index.php/hello/user_test
You should see output:
added 2 users
• Now go back to phpMyAdmin: http://localhost/phpmyadmin/
• Browse the table ‘user’
Voila! Now you should be able see the 2 new records that just got created.
Day 2 – The Basics
Let’s get started and make sure we have a fresh CodeIgniter+Doctrine install ready for some
code.
Fresh Quick CodeIgniter+Doctrine Install
Fresh Install Instructions:
(if you want to use your files from Day 1, skip to the next section instead)
• Have your PHP+MySQL web server ready. Recommended: WAMP (for Mac:
MAMP)
• Download my CodeIgniter+Doctrine bundle (v1.0).
• Extract and put the ci_doctrine folder into your web folder.
• Create a database named ci_doctrine for our project.
• Make sure your database info is correct in:
system/application/config/database.php.
• Make sure base_url is correct in: system/application/config/config.php.
• See if its working: http://localhost/ci_doctrine/.
Done!
If you already have the files from Day 1:
(if you used the fresh install from the section above, skip this)
• Rename or Copy your install folder to: ci_doctrine. (I decided to do this so
we don’t keep changing our site url on every tutorial)
• Delete: system/application/models/user.php
• Delete: system/application/controllers/hello.php
• Drop table: user
• Edit: system/application/config/config.php
view source
print?
1 // in system/application/config/config.php
2 $config['base_url'] = "http://localhost/ci_doctrine/";
CodeIgniter URL structure
Url’s in CodeIgniter can look like these:
This url invokes the controller class named “CONTROLLER_NAME”, and call it’s method
(function) named “METHOD_NAME”:
http://localhost/ci_doctrine/index.php/CONTROLLER_NAME/METHOD_NAME
Same as before, except it calls a method named index() by default:
http://localhost/ci_doctrine/index.php/CONTROLLER_NAME
Same as before. This time, it passes a “VALUE” as an argument to the controller method:
http://localhost/ci_doctrine/index.php/CONTROLLER_NAME/METHOD_NAME/VALUE
The value can be a number or a string.
You can keep appending more values in the url for passing additional arguments.
For More Info: Passing uri segments
CodeIgniter MVC (Model – View – Controller)
Models
We are going to be using Doctrine Model‘s, instead of CodeIgniter. I will explain it later in the
tutorial.
If you still want to learn about CodeIgniter Models, read: CodeIgniter Models
Views
• Views are created under system/application/views, and are named like
my_view.php
• They are the output templates. They can contain html, javascript and
more.
• Views also usually contain inline PHP code. (to display messages, run loops
etc…)
• Controllers typically load views for displaying output.
Official docs: CodeIgniter Views
Controllers
We already covered this in Day 1. Look for the section called “CodeIgniter Crash Course:
Controllers”.
Controller and View together
Here is a sample View (system/application/views/my_view.php):
view source
print?
01 Some plain text.
02
03 <div>
04 You can use HTML.
05 </div>
06
07 <div>
08 Display variables passed from a controller: <br />
11 </div>
12
15
16 echo "Counter shows: $i <br />";
17
18 } ?>
19
20 Or in alternate syntax: <br />
03 function index() {
04 $data = array();
07
08 $this->load->view('my_view',$data);
09 }
10
11 }
Both of these url’s will work:
http://localhost/ci_doctrine/index.php/sample_controller/index
http://localhost/ci_doctrine/index.php/sample_controller
Browser would display:
Some plain text.
You can use HTML.
Display variables passed from a controller:
index was called
blah blah
You can even do loops:
Counter shows: 0
Counter shows: 1
Counter shows: 2
Or in alternate syntax:
Counter shows: 0
Counter shows: 1
Counter shows: 2
Note:
• View contains a combination of raw output and simple inline PHP.
• index() is the default Controller function, so we don’t have to put it in the
url.
• $this->load->view(‘my_view’,$data) loads the view and outputs it to the
browser.
• First argument ‘my_view’ is the name of the view file, without the .php
part.
• Second argument $data is an array, which passes variables to the view.
• Example: $data['message'] becomes $message, and
$data['another_message'] becomes $another_message, in our view.
(If you created the files above, to test the code, you can delete them now. We’re not going to use
them again in our project.)
Doctrine Models
Models are classes that represent data (typically from your database).
For example, you might have a user table. So we can build a Model Class named “User” to
represent the records in that table.
Our Model class should be able to peform CRUD (Create, Read, Update and Delete) operations.
Luckily, Doctrine will be a great help in accomplishing this with minimal and clean code.
Differences in Usage (compared to CodeIgniter Models)
• They extend the Doctrine_Record class (instead of the “Model” class).
• They can be loaded like this: $u = new User(); (instead of this: $u = $this-
>load->model(‘user’);)
thanks to the Doctrine autoload we registered in our plugin.
• It is not PHP4 compatible.
That is all you need to know for now. It should be an easy transition for those of you already
working with CodeIgniter.
What does a Doctrine Model look like?
Here is a little PREVIEW of the kind of Model’s we are going to be building. We’ll get into
more details in the next tutorials.
view source
print?
01 <?php
02 class User extends Doctrine_Record
03 {
04 // define table columns in this function
11 ));
12
17
18 $this->hasColumn('referer_id', 'integer', 4);
19 }
20
23
24 // creates a relationship with a model named Post
25 $this->hasMany('Post as Posts', array(
26 'local' => 'id',
29
30 // can even have a relationship with itself
35
// causes 'created_at' and 'updated_at' fields to be updated
36
automatically
37 $this->actAs('Timestampable');
38
41
42 }
43
44 // a mutator function
47 }
48
49 }
Once we create Doctrine Model’s like this, we can do all kinds of database operations. Doctrine
can even create the table based on the model information alone.
Also keep in mind that there are other ways of creating Doctrine Models, such as using schema
files. But we will not get into that until later.
Day 3 – User Signup Form
Let’s build a Doctrine Model
Since we are building a user signup form today, it is best to start with a “User” Model.
Our model will be extending the Doctrine_Record class, which will represent records in a table
named “user”.
So let’s make one:
• Create a file named user.php under system/application/models
• We name the class User, matching the file name.
The class name is capitalized, but the filename is always lowercase. (This
is a CodeIgniter rule, rather than Doctrine, but we’re still going go with it for
consistency.)
• We add a method named setTableDefinition(), which will contain column
info.
• We also add a method named setUp(), which will contain additional options.
system/application/models/user.php
view source
print?
01 <?php
02 class User extends Doctrine_Record {
03
04 public function setTableDefinition() {
09
10 public function setUp() {
11 $this->setTableName('user');
12 $this->actAs('Timestampable');
13 }
14 }
03
04 public function setTableDefinition() {
09 }
10
13 $this->actAs('Timestampable');
14 $this->hasMutator('password', '_encrypt_password');
15 }
16
21 }
First we added a Mutator function named _encrypt_password to the password column.
Then we implemented it as a method inside our Model class.
Note: For mutators to work, auto_accessor_override option needs to be enabled. We have
already done it, in our plugin file doctrine_pi.php.
Let Doctrine create the tables
Why create the user table manually ourselves when we can just let Doctrine do it for us.
We will use the Doctrine static class to create our tables like this:
view source
print?
1 Doctrine::createTablesFromModels();
So let’s create a simple and reusable script that we can rebuild our database with.
• Create this controller:
system/application/controllers/doctrine_tools.php
view source
print?
01 <?php
02 class Doctrine_Tools extends Controller {
03
04 function create_tables() {
05 echo 'Reminder: Make sure the tables do not exist already.<br />
06 <form action="" method="POST">
09 if ($this->input->post('action')) {
10 Doctrine::createTablesFromModels();
11 echo "Done!";
12 }
13 }
14
15 }
Only pay attention to the highlighted line for now. It simply reads our Doctrine Model class files,
and creates tables for them in the database.
I know the code is a bit ugly, but we’re just going to use this controller temporarily as we write
our initial models.
• First make sure user table does NOT exist. If it does, drop it using
phpmyadmin.
• Go to: http://localhost/ci_doctrine/index.php/doctrine_tools/create_tables and
click the button.
• Check out our new table.
Note: The id column got generated automatically. We already have a setting in our
doctrine_pi.php plugin file:
view source
print?
1 // set the default primary key to be named 'id', integer, 4 bytes
2 Doctrine_Manager::getInstance()->setAttribute(
3 Doctrine::ATTR_DEFAULT_IDENTIFIER_OPTIONS,
03 | Index File
04 |--------------------------------------------------------------------------
05 |
06 | Typically this will be your index.php file, unless you've renamed it to
07 | something else. If you are using mod_rewrite to remove the page set this
08 | variable so that it is blank.
09 |
10 */
11 $config['index_page'] = "";
That’s it!
To make sure it’s working, visit: http://localhost/ci_doctrine/welcome.
You should see the welcome page.
Building the Signup Form
Before we start coding, let’s decide how we’re going to do this
The Game plan
We want a Controller named signup, so the url for the signup form becomes
http://localhost/ci_doctrine/signup
We would like this signup controller to load a View named signup_form by default, which will
contain the html for the form.
The form will submit to a method named submit inside the signup controller i.e. signup/submit.
From there we need to validate the user input. Display errors if needed.
Finally save the new user to the database using our new Doctrine Model.
“Signup” Controller
• Create this file: system/application/controllers/signup.php
view source
print?
01 <?php
02
07 }
08
11 }
12
13 }
Now let’s go over the code.
•First we created a constructor method (line 5). This will get called first every
time.
Constructors are optional, however there is a reason I decided to create one,
which we will see in a bit.
• In the constructor, we always must call the parent constructor first (line 6).
• In the index() method, (which acts as the default method for the controller),
we load a View named signup_form.
“signup_form” View
• Create this file: system/application/views/signup_form.php
view source
print?
01 <!DOCTYPE html>
02 <html lang="en">
03 <head>
04 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
05 <title>Signup Form</title>
06 </head>
07 <body>
08
09 <div id="signup_form">
10
13 </div>
14
15 </body>
16 </html>
For now it’s just a simple HTML file.
• Go to: http://localhost/ci_doctrine/signup
You should see the output:
New User Signup
CodeIgniter Helpers
Helpers are located in system/helpers. They contain procedural utility functions that make our
lives easier.
In this tutorial, we will be using the url and form Helpers.
They can be loaded like this:
view source
print?
1 $this->load->helper('url');
Or load multiple helpers at once:
view source
print?
1 $this->load->helper(array('form','url'));
CodeIgniter Libraries
Libraries are very similar to Helpers, but they tend to be Object Oriented instead.
We will be using the form_validation library, and load it like this:
view source
print?
1 $this->load->library('form_validation');
Once it’s loaded, we can access it like this:
view source
print?
1 $this->form_validation->//...
Add them to the controller
• Edit: system/application/controllers/signup.php
view source
print?
01 <?php
02
07
08 $this->load->helper(array('form','url'));
09 $this->load->library('form_validation');
10 }
11
12 public function index() {
13 $this->load->view('signup_form');
14 }
15
16 }
Note: When you load Helpers and Libraries inside the Constructor, they become available in all
methods of that Controller. For example, now index() function can use them.
Note2: When Views are loaded, they also can use the Helpers and Libraries that the Controller
had loaded.
Form Helper
Now that we have it loaded, let’s use our Form Helper functions.
• Edit: system/application/views/signup_form.php
view source
print?
01 <!DOCTYPE html>
02 <html lang="en">
03 <head>
04 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
05 <title>Signup Form</title>
06 </head>
07 <body>
08
09 <div id="signup_form">
10
15 <p>
16 <label for="username">Username: </label>
19 <p>
20 <label for="password">Password: </label>
23 <p>
24 <label for="passconf">Confirm Password: </label>
27 <p>
28 <label for="email">E-mail: </label>
31 <p>
32 <?php echo form_submit('submit','Create my account'); ?>
33 </p>
34
37
38 </body>
39 </html>
I highlighted all the different kinds of functions we just called. These functions create the html
for our form tags and elements.
• Open http://localhost/ci_doctrine/signup
And in the source code you can see:
view source
print?
01 <!DOCTYPE html>
02 <html lang="en">
03 <head>
04 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
05 <title>Signup Form</title>
06 </head>
07 <body>
08
09 <div id="signup_form">
10
21 <p>
22
25 <p>
26 <label for="email">E-mail: </label>
31
32 </div>
33
34 </body>
35 </html>
Let's give it Style
• Create a folder named css directly under our project folder ci_doctrine
• Create file: css/style.css
view source
print?
01 body {
02 font-family: "Trebuchet MS",Arial;
03 font-size: 14px;
04 background-color: #212426;
05 color: #11151E;
06 }
07
08 input, textarea, select {
09 font-family:inherit;
10 font-size:inherit;
11 font-weight:inherit;
12 }
13
14 #signup_form {
15 margin-left: auto;
16 margin-right: auto;
17 width: 360px;
18
19 font-size: 16px;
20
21 }
22
23 #signup_form .heading {
24 text-align: center;
25 font-size: 22px;
26 font-weight: bold;
27 color: #B9AA81;
28 }
29
30 #signup_form form {
31
32 background-color: #B9AA81;
33 padding: 10px;
34
35 border-radius: 8px;
36 -moz-border-radius: 8px;
37 -webkit-border-radius: 8px;
38
39 }
40
43 }
44
47 font-weight: bold;
48 padding: 8px;
49
50 border: 1px solid #FFF;
51 border-radius: 4px;
52 -moz-border-radius: 4px;
53 -webkit-border-radius: 4px;
54 }
55
56 #signup_form form input[type=submit] {
57 display: block;
58 margin: auto;
59 width: 200px;
60 font-size: 18px;
61 background-color: #FFF;
62 border: 1px solid #BBB;
63 }
64
67 }
68
69 #signup_form .error {
70 font-size: 13px;
71 color: #690C07;
72 font-style: italic;
73 }
This tutorial is not about CSS, so there is not much to explain here.
• Edit: system/application/views/signup_form.php again.
• Add the highlighted lines:
view source
print?
1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
5 <title>Signup Form</title>
6 <link rel="stylesheet" href="<?php echo base_url(); ?>css/style.css"
7 type="text/css" media="all">
8 </head>
9 <body>
As you can see, we just called the base_url() function, which returns http://localhost/ci_doctrine/
in this case. This function is part of the URL Helper.
• Go to: http://localhost/ci_doctrine/signup, you will see:
Form Validation
If you press the submit button now, you will receive a 404 error:
07
08 $this->load->helper(array('form','url'));
09 $this->load->library('form_validation');
10 }
11
12 public function index() {
13 $this->load->view('signup_form');
14 }
15
16 public function submit() {
17
18 if ($this->_submit_validate() === FALSE) {
19 $this->index();
20 return;
21 }
22
23 $this->load->view('submit_success');
24
25 }
26
27 private function _submit_validate() {
28
29 // validation rules
30 $this->form_validation->set_rules('username', 'Username',
31 'required|alpha_numeric|min_length[6]|max_length[12]');
32
33 $this->form_validation->set_rules('password', 'Password',
34 'required|min_length[6]|max_length[12]');
35
36 $this->form_validation->set_rules('passconf', 'Confirm Password',
37 'required|matches[password]');
38
39 $this->form_validation->set_rules('email', 'E-mail',
40 'required|valid_email');
41
42 return $this->form_validation->run();
43
44 }
45 }
Let's go over all the code we just added:
• We add 2 methods: submit() and _submit_validate().
• submit() get's called when the form is submitted to signup/submit.
First it validates the input. If it fails, it calls the index() method, which display
the Signup Form again.
• If no errors found, we load a View named submit_success, which we will
create in a bit.
• _submit_validate() is just a private function we created, that contains the
form validation stuff.
And it returns the result of the Validation (true or false).
Let's look at how the form validation functions work:
When we loaded the Form_Validation Library earlier
First we set the rules like this:
view source
print?
1 $this->form_validation->set_rules('username', 'Username',
2 'required|alpha_numeric|min_length[6]|max_length[12]');
The first parameter is the name of the form field.
The second parameter is the literal name for it, for display purposes.
The third parameter is a list of validation rules, separated by the pipe character "|".
You can see a list of validation rules here.
Finally we the run the validation by calling run() (line 42), which will return FALSE if a user
input does not validate.
Displaying Validation Errors
Next thing we need to do is display the errors to the user.
• Edit: system/application/views/signup_form.php
view source
print?
01 <!DOCTYPE html>
02 <html lang="en">
03 <head>
04 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
05 <title>Signup Form</title>
07 type="text/css" media="all">
08 </head>
09 <body>
10
11 <div id="signup_form">
12
19 <p>
20 <label for="username">Username: </label>
23 <p>
24 <label for="password">Password: </label>
27 <p>
28 <label for="passconf">Confirm Password: </label>
31 <p>
32 <label for="email">E-mail: </label>
35 <p>
36 <?php echo form_submit('submit','Create my account'); ?>
37 </p>
38 <?php echo form_close(); ?>
39
40 </div>
41
42 </body>
43 </html>
validation_errors() displays a list of the errors returned from the form validation (line 17).
The first and second arguments we passed are the html codes to be used to enclose each error
output.
Also, On lines 21 and 33 we now pass a second argument to the form_input() function call.
This way, when the form is re-displayed to the user, it will be populated with the values he
entered previously, so he doesn't have to start all over again.
Submit Success View
We will keep this one simple for now.
• Create: system/application/views/submit_success.php
view source
print?
1 Success!
Test The Form
• Enter some invalid data into the form and submit.
http://localhost/ci_doctrine/signup
And if you enter everything correctly, you should see the Success! message.
Saving the User
Now that our form works, we're going save the new user to the database using our Doctrine
Model.
• Edit the submit() function under
system/application/controllers/signup.php:
view source
print?
01 <?php
02
03 class Signup extends Controller {
04 // ...
05
06 public function submit() {
07
08 if ($this->_submit_validate() === FALSE) {
09 $this->index();
10 return;
11 }
12
13 $u = new User();
14 $u->username = $this->input->post('username');
15 $u->password = $this->input->post('password');
16 $u->email = $this->input->post('email');
17 $u->save();
18
19 $this->load->view('submit_success');
20
21 }
22
23 // ...
24 }
As you can see, all we do is create a User object, assign the parameters, and call save(), and
Doctrine should save the new record to the database. It's that simple.
Note: We are using $this->input->post() to get the submitted form values. When working with
CodeIgniter, we do NOT use superglobals such as $_POST directly. This provides added
security benefits.
Note2: We do NOT use any SQL filtering such as mysql_real_escape_string() when assigning
the user input to the Doctrine Model, because Doctrine will take care of filtering for us.
Test the Form
• Go to: http://localhost/ci_doctrine/signup, and submit it with proper values.
• Check the contents of the table:
3 Stack trace:
#0
C:\wamp\www\ci_doctrine\system\application\plugins\doctrine\lib\Doctrine\Conn
4 ection\Statement.php(253): Doctrine_Connection-
>rethrowException(Object(PDOException),
Object(Doctrine_Connection_Statement))
#1
5 C:\wamp\www\ci_doctrine\system\application\plugins\doctrine\lib\Doctrine\Conn
ection.php(1049): Doctrine_Connection_Statement->execute(Array)
#2
6 C:\wamp\www\ci_doctrine\system\application\plugins\doctrine\lib\Doctrine\Conn
ection.php(693): Doctrine_Connection->exec('INSERT INTO use...', Array)
#3
C:\wamp\www\ci_doctrine\system\application\plugins\doctrine\lib\Doctrine\Conn
7
ection\UnitOfWork.php(595): Doctrine_Connection-
>insert(Object(Doctrine_Table), Array)
#4 C:\wamp\www\ci_doctrine\system\application\plugins\doctrine\lib\Doctrine
in
8
<b>C:\wamp\www\ci_doctrine\system\application\plugins\doctrine\lib\Doctrine\
Connection.php</b> on line <b>1084</b><br />
The problem is, we tried to insert a duplicate value into a unique table column.
Handling Duplicates
We could simply check for the existence of a duplicate with something like this:
view source
print?
1 $user_table = Doctrine::getTable('User');
2 if ($user_table->findOneByUsername($username)) {
This is the first time we are looking into fetching records using Doctrine.
In the first line, we get the table object for our User Model. The name we pass is the name of
the Model, and NOT the name of the table. This is important to know, in case your model and
tables are named differently.
Then we call a magic function named findOneBy*(). It is magic, because it can be called on any
other property, like findOneByEmail(). You basically need to append the property name, (which
can be in camelCase format).
We could just add this code to our _submit_validate() function, for both username and email
fields, however that's not quite what I want to do.
Extending CodeIgniter Libraries
Since we're using Doctrine to save time and avoid code duplication, it's only fitting that we
continue with the same idea here.
We might need to check for duplicate records for other Models or from other Controllers later
on. So we're going to build a reusable form validation rule, by extending the Form Validation
class.
This way, other forms will have access to the same functionality later on, without having to
duplicate code.
• Create: system/application/libraries/MY_Form_validation.php
view source
print?
01 <?php if (!defined('BASEPATH')) exit('No direct script access allowed');
02
09 $CI->form_validation->set_message('unique',
10 'The %s is already being used.');
11
12 list($model, $field) = explode(".", $params, 2);
13
14 $find = "findOneBy".$field;
15
16 if (Doctrine::getTable($model)->$find($value)) {
17 return false;
18 } else {
19 return true;
20 }
21
22 }
23 }
Let's go over the code:
• Line 3: The new class needs to have the prefix MY_, and extends the core
class named CI_Form_validation.
• Line 5: We are going to create a validation rule named unique. With this
form_validation class, names of the methods match the names of the rules,
due to how the parent class is setup.
• Line 5: First argument $value is value of the form field input.
• Line 5: Second argument $params is the parameter passed to the rule. Our
rule will have this structure: unique[model.field]. You will see it in a bit.
• Lines 7-9: We need to get the CodeIgniter super object, so that we can
access the form_validation instance, and set the error message.
• Lines 12+: extract from the model.field values, build the name of the
findOneBy function, and check for existing records.
You can read more about creating libraries in the docs.
Using the new form validation rule
• Edit 2 lines in: system/application/controllers/signup.php
view source
print?
01 <?php
02
05 // ...
06
09 // validation rules
10 $this->form_validation->set_rules('username', 'Username',
'required|alpha_numeric|min_length[6]|max_length[12]|
11
unique[User.username]');
12
13 // ...
14
15 $this->form_validation->set_rules('email', 'E-mail',
16 'required|valid_email|unique[User.email]');
17
18 // ...
19 }
20 // ...
21 }
As you can see, now we can use our new form validation rule named unique. We also provide
the model name and the field name to this rule, in square brackets.
Test the form again
• Go to: http://localhost/ci_doctrine/signup
• Try to signup with the same values twice.
You will see 2 errors:
Day 4 – User Login
Login Form View
First thing we are going to do is to create the View for the Login Form.
We are going to make this one very similar to the Signup Form we created in the last article.
• Create: system/application/views/login_form.php
view source
print?
01 <!DOCTYPE html>
02 <html lang="en">
03 <head>
04 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
05 <title>Login Form</title>
07 type="text/css" media="all">
08 </head>
09 <body>
10
11 <div id="signup_form">
12
23 <p>
24 <label for="password">Password: </label>
27 <p>
28 <?php echo form_submit('submit','Login'); ?>
29 </p>
30
35
36 </div>
37
38 </body>
39 </html>
All I did was copy the contents of the signup form, change the page title, remove some form
fields and added a link at the bottom to the signup form.
anchor()
On Line 33 in the file above, I used a function named anchor(). This is part of the URL Helper
in CodeIgniter. It helps us create links with ease. The first argument is the Controller Name,
and the second argument is the Link Text. You can also pass an optional third argument for
extra html attributes. The result is:
view source
print?
1 <a href="http://localhost/ci_doctrine/signup">Create an Account</a>
Style the links
I am just going to make a small addition to the stylesheet so our links look better.
• Edit: css/style.css
view source
print?
01 body {
02 font-family: "Trebuchet MS",Arial;
03 font-size: 14px;
04 background-color: #212426;
05 color: #11151E;
06 }
07
08 a {
09 color: #FFF;
10 }
11
12 a:hover {
13 color: #B9AA81;
14 }
15
16 /* ... */
I only added the highlighted lines.
Link from the Signup Form
Likewise, we will add a link from the Signup Form to the new Login Form.
• Edit: system/application/views/signup_form.php
view source
print?
01 <!DOCTYPE html>
02 <html lang="en">
03 <head>
04 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
05 <title>Signup Form</title>
07 type="text/css" media="all">
08 </head>
09 <body>
10
11 <div id="signup_form">
12
19 <p>
20 <label for="username">Username: </label>
23 <p>
24 <label for="password">Password: </label>
27 <p>
28 <label for="passconf">Confirm Password: </label>
31 <p>
32 <label for="email">E-mail: </label>
35 <p>
36 <?php echo form_submit('submit','Create my account'); ?>
37 </p>
38 <?php echo form_close(); ?>
39 <p>
40 <?php echo anchor('login','Login Form'); ?>
41 </p>
42
43 </div>
44
45 </body>
46 </html>
I only added the highlighted lines.
Note: We just linked to a Controller named login. This Controller does not exist yet. So let’s
create it.
Login Controller
First we are going create a sort of skeleton structure for our new Controller.
• Create: system/application/controllers/login.php
view source
print?
01 <?php
02 class Login extends Controller {
03
04 public function __construct() {
05 parent::Controller();
06
07 $this->load->helper(array('form','url'));
08 $this->load->library('form_validation');
09 }
10
13 }
14
19 return;
20 }
21
22 redirect('/');
23
24 }
25
26 private function _submit_validate() {
27
28 }
29
30 }
This looks very similar to the submit Controller we created in the last article. The only new
thing is the highlighted line.
redirect()
This function is part of the URL Helper. It forwards the surfer to a specified Controller. In this
case we only passed ‘/’, which will basically send the surfer to the Home Page or in other words
the Default Controller.
So the user will end up at http://localhost/ci_doctrine/.
Test The Login Controller+View
• Go here: http://localhost/ci_doctrine/login
3
4 public function index() {
7
8}
Routes File
• Edit: system/application/config/routes.php
Changing just one line:
view source
print?
1 // ...
2 $route['default_controller'] = "home";
3 $route['scaffolding_trigger'] = "";
4 // ...
That’s it. Now when you go to http://localhost/ci_doctrine/, you will see our controller:
Home Sweet Home!
The Routes File has some other purposes, which we will not get into right now. But you can read
more about it here.
User Login (Authentication)
Now that our forms are in place, we need to add authentication functionality to our application.
First I want to show you how to accomplish it in a simple and most common way. Once we
cover that, I will show you a better way of handling this.
Authentication with Form Validation
• Edit: system/application/controllers/login.php
view source
print?
01 <?php
02 class Login extends Controller {
03
04 public function __construct() {
05 parent::Controller();
06
07 $this->load->helper(array('form','url'));
08 $this->load->library('form_validation');
09 }
10
13 }
14
21
22 redirect('/');
23
24 }
25
26 private function _submit_validate() {
27
28 $this->form_validation->set_rules('username', 'Username',
29 'trim|required|callback_authenticate');
30
31 $this->form_validation->set_rules('password', 'Password',
32 'trim|required');
33
$this->form_validation->set_message('authenticate','Invalid login.
34
Please try again.');
35
36 return $this->form_validation->run();
37
38 }
39
40 public function authenticate() {
41
42 // get User object by username
if ($u = Doctrine::getTable('User')->findOneByUsername($this->input-
43
>post('username'))) {
44
47 $u_input->password = $this->input->post('password');
48
51 unset($u_input);
52 return TRUE;
53 }
54 unset($u_input);
55 }
56
57 return FALSE;
58 }
59 }
Highlighted functions have been updated.
First I added some form validation rules inside the _submit_validate() function, like in the last
article. But this time I added a Callback rule. Notice on Line 29: callback_authenticate. The
format is callback_[function_name].
On Line 34 I set a custom message for this validation rule, in case it fails.
Then I added the function named authenticate. This callback function needs to return TRUE
when everything is good, and FALSE when the validation fails.
Lines 43-53: This is where I perform the username and password check for the login. First I
attempt to get the User Record searching by the input username. (We learned about the
getTable and findOneBy* functions in the last article).
Applying the Mutator to an external value
Remember we added a Mutator to the password field in the last article?
Since the stored passwords are encrypted, I need to apply the same encryption to the user input,
before I can compare the two.
That’s why I create a new User object named $u_input. on Line 46 and assigned the input
password to it. This is how I trigger the Mutator function on the password that was entered in
the form. (If anyone knows a better of doing this, let me know!)
Now the encrypted version of the input password resides in $u_input->password. So I was able
to compare it to $u->password.
I also unset the $u_input object, just to make sure it doesn’t get saved as a new record by
accident.
Testing the Form
First create a new user by going to the Signup Form at http://localhost/ci_doctrine/signup.
Now go to: http://localhost/ci_doctrine/login and use a wrong login. You should see:
When you use a correct login, you should get forwarded back to the home page.
Home Sweet Home!
Our Login Form works!
Now the Better Way!
I mentioned I was going to change things a bit and show you how to do this in a better way.
What’s wrong with the current method?
1. I don’t like how the authentication code is all inside the Controller. This is
not good for code reusability.
2. It should be part of the User Model, since it’s directly related to it.
3. We stored the User Record in a variable named $u. But it’s inside a function
and not global. So, if other Controllers need to use it, they can’t.
4. Turning the object $u directly into a Global Variable would be a bad design
decision.
5. We do not want to store the whole user object inside the session. Doctrine
Models have a lot of internal data, which could quickly inflate the size of our
session storage. Only thing we are going store in the session is the user_id.
Singleton Pattern to the rescue!
Some of you may not know what Singleton Pattern means. It’s one of the most popular design
patterns, and I will show you how we are going to use it.
With this new class I am going to create, we will have a better design for our authentication
functionality, and we will also make the User Model instance for the logged in user globally
available in our application.
Singleton Pattern for Current User
First let’s build the skeleton:
• Create: system/application/models/current_user.php
view source
print?
01 <?php
02 class Current_User {
03
04 private static $user;
05
06 private function __construct() {}
07
09
10 if(!isset(self::$user)) {
11
12 // put the User record into $this->user
13 // ...
14
15 }
16
17 return self::$user;
18 }
19
20 public function __clone() {
23
24 }
Now let’s go over the code:
• There is a reason I called the class Current_User. Because this static class
will always return an instance of the User class, for the current logged in
user.
• This class does NOT extend Doctrine_Record, because we are not really
creating a Doctrine Model.
• The $user variable will contain the User object for the logged in user. And we
will access it by Current_User::user(). That is the syntax for calling a static
class function.
• We will never create an instance of this Current_User class, so the
constructor is set to private.
• We also disallow cloning of this class so the __clone() function triggers an
error.
Now I’m going to add a bit more code to make this functional.
• Edit: system/application/models/current_user.php
view source
print?
01 <?php
02 class Current_User {
03
04 private static $user;
05
06 private function __construct() {}
07
11
12 $CI =& get_instance();
13 $CI->load->library('session');
14
15 if (!$user_id = $CI->session->userdata('user_id')) {
16 return FALSE;
17 }
18
19 if (!$u = Doctrine::getTable('User')->find($user_id)) {
20 return FALSE;
21 }
22
23 self::$user = $u;
24 }
25
26 return self::$user;
27 }
28
37
38 // password match (comparing encrypted passwords)
39 if ($u->password == $u_input->password) {
40 unset($u_input);
41
42 $CI =& get_instance();
43 $CI->load->library('session');
44 $CI->session->set_userdata('user_id',$u->id);
45 self::$user = $u;
46
47 return TRUE;
48 }
49
50 unset($u_input);
51 }
52
53 // login failed
54 return FALSE;
55
56 }
57
58 public function __clone() {
61
62 }
Let’s go over the code in user() method first:
• The usage for this function is: Current_User::user() and it returns the User
object for the logged in user.
• Line 10: We check to see if the user object is already there. If it is, we return
it at line 26 .
• Lines 12-17: We load the session library, so we can read/write in the
session. If the user is logged in, his id should be stored there. If we don’t see
the user_id, it returns FALSE.
• We had to use the $CI object to load the session library because this is a
static class.
• Lines 19-21: Fetch the User object by id. If we can’t, return FALSE. Note that
we used a function named find() to look up by id.
• Lines 23 and 26: Everything went well, so we store the user object in the
static variable $user and return it.
Now, when we call Current_User::user() , we can get the User object as if it’s a global
variable. And if it returns false, it means the user is NOT logged in.
Let’s go over the login() method:
• I copied most of the code from the login Controller we created earlier. The
code is from the authenticate() method.
• I added loading of the session library at line 42-43, so I could store the
user_id in the session at line 44
• I also added line 45. Once the user gets logged in, the $user static variable
is set, so it can be returned by the user() function later.
By the way, we just learned how to use Sessions in CodeIgniter. It’s not very complicated. All
you need to do is to load the library and you can use the userdata() and set_userdata() function
to read and write the variables. You can read more about it here.
Now we can call the static function like this: Current_User::login($username,$password) ,
and it will take care of the rest.
Phew! I hope that was not very complicated. But I’m sure you will soon understand why we
went through all of that.
Simplified Login Controller
We are about the see the first benefit of creating all that code above.
• Edit: system/application/controllers/login.php
Only editing the authenticate() function:
view source
print?
01 <?php
02 class Login extends Controller {
03
04 // ...
05
06 public function authenticate() {
07
08 return Current_User::login($this->input->post('username'),
09 $this->input->post('password'));
10
11 }
12 }
3 // ...
Let’s also autoload the URL Helper.
• Edit: system/application/config/autoload.php
Find this line:
view source
print?
1 // ...
2 $autoload['helper'] = array('url');
3 // ...
Now that’s out of the way.
Logout Controller
• Create: system/application/controllers/logout.php
view source
print?
01 <?php
02 class Logout extends Controller {
03
04 public function index() {
05
06 $this->session->sess_destroy();
07 $this->load->view('logout');
08 }
09
10 }
We remove all of the session data by calling $this->session->sess_destroy(). Now the user will
no longer be logged in.
Logout View
• Create: system/application/views/logout.php
view source
print?
01 <!DOCTYPE html>
02 <html lang="en">
03 <head>
04 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
05 <title>Logout</title>
06 <link rel="stylesheet" href="<?php echo base_url(); ?>css/style.css"
07 type="text/css" media="all">
08 <meta http-equiv="refresh" content="3;url=<?php echo base_url(); ?>">
09 </head>
10 <body>
11
12 <div>
13
14 <p>
17 <p>
18 Redirecting you <?php echo anchor('/','home'); ?>.
19 </p>
20
21 </div>
22
23 </body>
24 </html>
This logout page will display a short message and redirect the surfer using a meta refresh.
Style Changes
I have made some css changes, so just copy paste the following:
• Edit: css/style.css
view source
print?
01 body {
02 font-family: "Trebuchet MS",Arial;
03 font-size: 14px;
04 background-color: #212426;
05 color: #B9AA81;
06 }
07
08 a {
09 color: #FFF;
10 }
11
12 a:hover {
13 color: #B9AA81;
14 }
15
16 input, textarea, select {
17 font-family:inherit;
18 font-size:inherit;
19 font-weight:inherit;
20 }
21
22 #signup_form {
23 margin-left: auto;
24 margin-right: auto;
25 width: 360px;
26
27 font-size: 16px;
28
29 }
30
31 #signup_form .heading {
32 text-align: center;
33 font-size: 22px;
34 font-weight: bold;
35 color: #B9AA81;
36 }
37
38 #signup_form form {
39
40 background-color: #B9AA81;
41 padding: 10px;
42
43 border-radius: 8px;
44 -moz-border-radius: 8px;
45 -webkit-border-radius: 8px;
46
47 }
48
51 color: #11151E;
52 }
53
54 #signup_form form input[type=text],input[type=password] {
55 width: 316px;
56 font-weight: bold;
57 padding: 8px;
58
61 -moz-border-radius: 4px;
62 -webkit-border-radius: 4px;
63 }
64
67 margin: auto;
68 width: 200px;
69 font-size: 18px;
70 background-color: #FFF;
73 }
74
77 }
78
79 #signup_form .error {
80 font-size: 13px;
81 color: #690C07;
82 font-style: italic;
83 }
Home View
Now we are going to see how easy it is to fetch user information for the logged in user.
• Create: system/application/views/home.php
view source
print?
01 <!DOCTYPE html>
02 <html lang="en">
03 <head>
04 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
05 <title>Home</title>
07 type="text/css" media="all">
08 </head>
09 <body>
10
11 <div>
12
21 </div>
22
23 </body>
24 </html>
Home Controller
• Edit: system/application/controllers/home.php
view source
print?
1 <?php
2 class Home extends Controller {
3
4 public function index() {
5 $this->load->view('home');
6 }
7
8}
That’s it!
Let’s Test It
• Go to: http://localhost/ci_doctrine/logout to make sure you are logged out.
First you will see:
• Enter a correct login info and you will be redirected home again.
Now you should see:
In this case my username is burak, and it was displayed back to me.
Voila!
Day 5 – Database CRUD
CRUD stands for Create, Read, Update and Delete, the basic operations
performed on the database. With regular CodeIgniter setups, most developers
would use the Active Record Class for performing such operations. But we are not
going to be doing that.
In Doctrine, there can be multiple ways of accomplishing the same task. This gives you
flexibility to pick the best approach suitable for each situation. Therefore we shall explore these
different methods for each part of the CRUD.
1. CREATE
The act of inserting new data/records into the database:
Record Object
This is the way we have been creating our user records in the previous tutorials. It’s pretty
simple:
view source
print?
1 $u = new User();
2
3 $u->username = 'myuser';
4 $u->password = 'mypass';
5
6 $u->save();
When the save() method is called, the new record is created.
fromArray()
Sometimes you have all the data already in an array. Instead of assigning every value to the
record object separately, you can simply use the fromArray() method.
view source
print?
01 $data = array(
02 'username' => 'myuser',
05 );
06
07 $u = new User();
08
09 $u->fromArray($data);
10
11 $u->save();
flush()
When you call the flush() method on the Doctrine_Connection object, all unsaved record
objects automatically get saved. And Doctrine performs this in a single transaction.
view source
print?
01 $u = new User();
02 $u->username = 'myuser';
03 $u->password = 'mypass';
04
07 $u2->password = 'foopass';
08
09 $conn = Doctrine_Manager::connection();
10
11 $conn->flush();
This way you don’t need to call the save() method for each object.
Raw SQL
We can just give a plain SQL query to the Doctrine_Connection object and call the execute()
method.
This also supports binding of values to the query (with the usage of ‘?’), which automatically
filters against SQL injection.
view source
print?
1 $data = array('myuser','mypass');
2
3 $conn = Doctrine_Manager::connection();
4
3 $u = Doctrine::getTable('User')->find($user_id);
4
5 echo $u->username;
findOneBy*()
This is a magic method. You can simply append the column name, and it will search the table by
that column.
For example, to find a user record by username:
view source
print?
1 $username = 'myuser';
2
3 $u = Doctrine::getTable('User')->findOneByUsername($username);
4
5 echo $u->username;
It fetches only one record.
findBy*()
Another magic method, which works similarly, but fetches multiple rows. It returns a
Doctrine_Collection object.
You can treat this returned object just like an array, to get to the individual record.
(let’s assume we have a column named ‘status’):
view source
print?
1 $users = Doctrine::getTable('User')->findByStatus('active');
2
3 echo $users[0]->username;
4 echo $users[1]->username;
Even if the actual column name is all lowercase, it can be used as capitalized in that magic
function call (status vs. Status).
DQL
This is our first look at DQL in these tutorials. It stands for Doctrine Query Language. It is
actually a major feature. Most advanced queries are performed using DQL.
In this example, we first create the query object, and add the components of the query to it. All
the method calls can be chained nicely. Finally we execute the query, which returns us a
Doctrine_Collection object, like that last example.
view source
print?
01 $status = 'active';
02
03 $q = Doctrine_Query::create()
04 ->select('username')
05 ->from('User')
06 ->where('status = ?', $status)
07 ->limit(20);
08
09 $users = $q->execute();
10
11 echo $users[0]->username;
12 echo $users[1]->username;
We will see many more examples of DQL in our project. But if you want more info right now,
there is a long documentation chapter here.
toArray()
A Doctrine_Record object contains a lot of stuff behind the scenes. If you ever try to print_r()
that object directly, you will see a long list of data.
However, when you simply want the data of the record, you can convert it to an array with the
toArray() call.
view source
print?
1 $user_id = 1;
2
3 $u = Doctrine::getTable('User')->find($user_id);
4
5 $u_arr = $u->toArray();
6
7 print_r($u_arr);
What you get will be similar to what you get from a mysql_fetch_assoc() call.
3. UPDATE
The act of updating existing records.
Record Object
You can make direct changes to any Doctrine_Record object. Once you call save(), it will
perform an UPDATE.
view source
print?
1 $user_id = 1;
2
3 $u = Doctrine::getTable('User')->find($user_id);
4
5 $u->password = 'newpassword';
6
7 $u->save();
DQL
When you want to update multiple rows at once, the preferred way is to use DQL.
view source
print?
1 $status = 'active';
2
3 $q = Doctrine_Query::create()
4 ->update('User')
3 $u = Doctrine::getTable('User')->find($user_id);
4
5 $u->delete();
DQL
When deleting multiple records, use DQL.
view source
print?
1 $q = Doctrine_Query::create()
2 ->delete('User')
5 $numrows = $q->execute();
6 echo "$numrows records deleted";
The execute() method call will return the number of deleted records.
Day 6 – Models with Relationships
Doctrine Model Relationships (a quick overview)
One of the key features of Doctrine is the handling of Relationships between Models. At this
point in our project, we will start utilizing it.
On a typical Message Board:
• There are Forums
• Forums have Threads
• Threads have Posts
• Users have Posts
We will learn how to define these relationships in our Models. Doctrine supports the following
types of relationships:
• One to Many (or Many to One)
• One to One
• Many to Many
• Tree Structure
• Nest Relations
Today we will be talking about only the first two.
Foreign Keys
To setup relationships between models (and tables in the database), we need to have certain
fields in our tables. These are used as Foreign Keys.
For example, if each Post belongs to a User, we need a user_id field in the Post table. We can
specify this in our model:
view source
print?
1 class Post extends Doctrine_Record {
2
5 $this->hasColumn('user_id', 'integer');
6 }
7
8}
But to specify the type of relationship, we are going to have to do more than that.
One to Many Relationships
Just like the name suggests, this creates a relationship between one instance of a Model and
multiple instances of another Model.
For example: one User has many Posts.
In Doctrine, this kind of relationship is setup using the hasOne() and hasMany() functions.
Post Example:
view source
print?
01 class Post extends Doctrine_Record {
02
05 $this->hasColumn('user_id', 'integer');
06 }
07
08 public function setUp() {
09 $this->hasOne('User', array(
10 'local' => 'user_id',
13 }
14
15 }
hasOne() says that the Post is associated with one User. The first parameter is the Model name
and the second parameter is an array that sets the foreign keys.
It links the two Models by the user_id field in the local Model (i.e. Post), with the id field in the
foreign Model (i.e. User).
Now we can access the User object for a post like this: $post->User. We’ll see more of that
later.
User Example:
view source
print?
01 class User extends Doctrine_Record {
02
07
08 public function setUp() {
13 }
14 }
On this side of the relationship, there are no additional fields for the foreign key. But we still
define it by the hasMany() call.
The first parameter looks a bit different this time. When we say Post as Posts, we link to the
Model named Post, but we name the property Posts.
This way we are going to access it like $user->Posts, instead of $user->Post. This is just a
matter of preference, because it sounds better.
Note: As a rule of thumb, the Model that has the hasOne() call is the one with the foreign key
field. (Post.user_id in this case).
One to One Relationships
This type of relationship is used when each Model is linked to only one instance of the other
Model.
For example, if you have separate Models/Tables for User and Email, and if you want each User
to have only one Email, you might use this.
The syntax is exactly the same as the One to Many Relationships, but this time both Models
call the hasOne() function, instead of hasMany().
for more info
This is all we needed to know to continue our project for now. But if you want to find out about
all types of relationships in Doctrine, you can read the whole documentation section on this
subject here.
Let’s Create Some Models
Let’s use what we just learned to create our new Models.
Note: I would like to build this project similar to the CodeIgniter Message Board. They also
seem to have Categories, that contain multiple Forums, so we are going to create a Model for
that too.
Category Model
• Create: system/application/models/category.php
view source
print?
01 <?php
02 class Category extends Doctrine_Record {
03
04 public function setTableDefinition() {
07
08 public function setUp() {
13 }
14 }
Looks simple. We have a Category Model that has a title, and can have many Forums. Also we
are expecting that the Forum Model to have a field named category_id.
Forum Model
• Create: system/application/models/forum.php
view source
print?
01 <?php
02 class Forum extends Doctrine_Record {
03
04 public function setTableDefinition() {
09
10 public function setUp() {
11 $this->hasOne('Category', array(
12 'local' => 'category_id',
19 }
20
21 }
As expected, we have a category_id field for the foreign key.
Also we have 2 relationships now. First one is with Category Model, as Forums have (or belong
to) a single Category. The second one is with Thread Model. Each Forum can have many
Threads.
Thread Model
• Create: system/application/models/thread.php
view source
print?
01 <?php
02 class Thread extends Doctrine_Record {
03
04 public function setTableDefinition() {
07 }
08
13 ));
14 $this->hasMany('Post as Posts', array(
19
20 }
Post Model
• Create: system/application/models/post.php
view source
print?
01 <?php
02 class Post extends Doctrine_Record {
03
04 public function setTableDefinition() {
09
10 public function setUp() {
11 $this->actAs('Timestampable');
12 $this->hasOne('Thread', array(
15 ));
16 $this->hasOne('User', array(
19 ));
20 }
21
22 }
Notice this time we have 2 foreign keys. Because a Post belongs to a Thread, and also belongs
to a User.
User Model
We already have this Model, we are just making changes.
• Edit: system/application/models/user.php
view source
print?
01 <?php
02 class User extends Doctrine_Record {
03
04 // ...
05
06 public function setUp() {
07 $this->setTableName('user');
08 $this->actAs('Timestampable');
09 $this->hasMutator('password', '_encrypt_password');
10
15 }
16 //...
17 }
Create the Tables
Remember our Controller that creates Tables from Models? It’s time to use that again.
• Go to: http://localhost/ci_doctrine/doctrine_tools/create_tables
• Press the button, and it should create the tables.
• Look at phpmyadmin at: http://localhost/phpmyadmin.
Looks great!
Now let’s see how we can add some data using these Models.
Creating Records
We are going to write a quick test controller to demonstrate how we can add data.
• Create: system/application/controllers/test.php
view source
print?
01 <?php
02 class Test extends Controller {
03
04 function index() {
05
06 $category = new Category();
09
10 $forum = new Forum();
11 $forum->title = "Introduce Yourself!";
$forum->description = "Use this forum to introduce yourself to the
12
CodeIgniter community, or to announce your new CI powered site.";
13
14 $forum->Category = $category;
15
16 $forum->save();
17 }
18
19 }
Do you see how intuitive it is?
$forum->Category represents the relationship that the Forum object has with the Category
Model. We can directly assign a Category object to it to link them.
Let’s expand this so we have a bit more data.
• Edit: system/application/controllers/test.php
view source
print?
01 <?php
02 class Test extends Controller {
03
04 function index() {
05
06 $category = new Category();
13 $forum->Category = $category;
14
27
28 $forum3 = new Forum();
31
32 $forum4 = new Forum();
35
36 $category2['Forums'] []= $forum3;
41 $conn->flush();
42
43 echo "Success!";
44
45 }
46
47 }
We just created 2 categories, and 2 forums in each of them, in a few different ways.
• Line 21: We add a Forum to the Category, instead of the other way around.
But this time, since $category->Forums contains multiple Forums, we push
to it like you would to an array, instead of direct assignment like on line 13.
• Lines 25-37: Here you can see how we can access all elements of the
Doctrine Model objects like array elements. Some people prefer this syntax.
• Lines 40-41: Instead of calling save() on every single object, we can just use
flush() to save everything.
Let’s test this.
• Go to: http://localhost/ci_doctrine/test
You should see the success message:
Success!
Now let’s look at the category table:
You can see our new 2 categories, with id’s 1 and 2. Let’s see if the forums got the correct
associations.
Look at the forum table:
Everything looks good! The Forums each have proper category_id’s assigned.
Day 7 – Fixtures & Forum List
First, Some Styling
Before we get started, let’s add some style so that pages look somewhat decent. Just copy-paste,
and don’t read it. CSS is not our focus today.
• Edit: css/style.css
view source
print?
001 body {
002 font-family: "Trebuchet MS",Arial;
007
008 a {
011
012 a:hover {
015
016 input, textarea, select {
017 font-family:inherit;
018 font-size:inherit;
019 font-weight:inherit;
020 }
021
022 /* FORUMS -----------------------------------------*/
023
024 div.forums {
027 }
028
029 .forums h2 {
030 font-size: 16px;
039 }
040
041 .forums h3 {
042 font-size: 15px;
049
050 .forum {
053 }
054
057 }
058
065 }
066
073
074 #signup_form form {
081
082 #signup_form form label {
085 }
086
105 }
106
109 }
110
115 }
Ok, now we’re ready to get more coding done!
The Home Page
We are about to work on the Controller and the View for our Home Page (i.e. Forum List). We
want this page to show all of the Forums broken down by Categories.
Home Controller
• Edit: system/application/controllers/home.php
view source
print?
01 <?php
02 class Home extends Controller {
03
04 public function index() {
05
06 $vars['categories'] = Doctrine::getTable('Category')->findAll();
07
08 $this->load->view('home', $vars);
09 }
10
11 }
As mentioned before, variables are passed to a View in an array, and the array indexes become
variable names. In other words, $vars['categories'] becomes $categories inside the View.
Doctrine::getTable(‘Category’)->findAll(); gives us all of the Category records, in the form of
a Doctrine_Collection object.
Home View
(I’m going build this page in a few steps)
• Edit: system/application/views/home.php
First we make a blank page, with a container div and a heading:
view source
print?
01 <!DOCTYPE html>
02 <html lang="en">
03 <head>
04 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
05 <title>Home</title>
07 type="text/css" media="all">
08 </head>
09 <body>
10
11 <div class="forums">
12
15 </div>
16
17 </body>
18 </html>
Next, we loop thru the $categories object, and display titles for each Category:
view source
print?
01 <!DOCTYPE html>
02 <html lang="en">
03 <head>
04 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
05 <title>Home</title>
07 type="text/css" media="all">
08 </head>
09 <body>
10
11 <div class="forums">
12
17 <div class="category">
18
21 </div>
22
25 </div>
26
27 </body>
28 </html>
Note: $categories is a Doctrine_Collection object here, but it can be treated like an array. So we
can use foreach on it.
Now we are going to show each Forum, their descriptions, and number of Threads.
view source
print?
01 <!DOCTYPE html>
02 <html lang="en">
03 <head>
04 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
05 <title>Home</title>
07 type="text/css" media="all">
08 </head>
09 <body>
10
11 <div class="forums">
12
17 <div class="category">
18
23 <div class="forum">
24
25 <h3>
29
30 <div class="description">
33
34 </div>
35
36 <?php endforeach; ?>
37
38 </div>
39
40 <?php endforeach; ?>
41
42 </div>
43
44 </body>
45 </html>
As you can see, getting all Forums under a Category is as easy as calling $category->Forums.
And again, we can treat it as an array and loop thru it.
Line 26: We are creating a link to the Forum. It will link to a Controller named Forums and
pass the id in the url. And the link text is the Forum title.
Note: In CodeIgniter, Controllers and Models cannot have the same name. That’s why I linked
to ‘forums’ instead of ‘forum’. Some people might prefer ‘view_forum’ or ‘show_forum’ etc…
Line 27: This is the first time we are using count(). That’s how we get the number of Threads
that belong to that Forum object.
The Result
• Go to: http://localhost/ci_doctrine/
You will see this:
Looks nice. But the links are broken since we haven’t created the Forums Controller (i.e.
Forum Page) yet.
Now, we could go ahead and build the Forum Page, but it’s just not going to look very good
since we have no Threads or Posts whatsoever. This is a great time to start talking about Data
Fixtures.
Doctrine Data Fixtures
What are Data Fixtures?
“Data fixtures are meant for loading small sets of test data through your models to populate
your database with data to test against.” (from here).
This will do the job for filling the database with some data, so we can do proper testing as we
develop our project.
In the last article, we created a Test Controller for inserting some data. That worked well for this
tutorial, and we saw some code examples along the way. But on a real development
environment, it’s better to just use Data Fixtures.
Creating Fixtures
First, we need to have a place to put our Fixtures. You can put this folder anywhere, but I would
like to create it directly under the applications folder.
• Create Folder: system/application/fixtures
• Create: system/application/fixtures/data.yml
That’s right, we are creating a YAML file. Don’t worry if you are not familiar with this file
format.
We are going to start small with an example:
view source
print?
1 User:
2 Admin:
3 username: Administrator
4 password: testing
5 email: [email protected]
6 Test:
7 username: TestUser
8 password: mypass
9 email: [email protected]
Even if you are not familiar with YAML, the structure is easy to understand.
This code just creates 2 User records.
Note: I used 2 spaces for indentation, but it can be any number of spaces. And Tabs do NOT
work.
• First line is the name of the Model we are loading data for.
• Line 2 and 6: These are just names for these records. They can be anything.
We can use them later in the fixture to reference these records.
• The rest should be obvious. We are assigning data to each field for these
records.
Now, we need to load this fixture into the database.
• Edit: system/application/controllers/doctrine_tools.php
view source
print?
01 <?php
02 class Doctrine_Tools extends Controller {
03
04 // ....
05
06 function load_fixtures() {
13 Doctrine_Manager::connection()->execute(
14 'SET FOREIGN_KEY_CHECKS = 0');
15
16 Doctrine::loadData(APPPATH.'/fixtures');
17 echo "Done!";
18 }
19 }
20
21 }
We added a function named load_fixtures() to this Controller.
Doctrine:loadData() does the work. Every time it is called, it will purge existing data from the
tables, and load the Data Fixtures from the given path. If you don’t want the table purge to
happen, you need to pass a second argument as true.
(I also had to disable FOREIGN_KEY_CHECKS to avoid some foreign key related errors
during the purge.)
• Go to: http://localhost/ci_doctrine/doctrine_tools/load_fixtures and click the
button.
• Now check the user table in phpmyadmin.
Now that everything seems to be working, let’s expand our fixture so we have more data to work
with.
• Edit: system/application/fixtures/data.yml
view source
print?
01 User:
02 Admin:
03 username: Administrator
04 password: testing
05 email: [email protected]
06 Test:
07 username: TestUser
08 password: mypass
09 email: [email protected]
10 Foo:
11 username: Foobar
12 password: mypass
13 email: [email protected]
14
15 Forum:
16 Forum_1:
21 Forum_2:
22 title: The Lounge
23 description: >
24 CodeIgniter's social forum where you can discuss anything not related
29 Forum_4:
30 title: Code and Application Development
31 description: >
32 Use the forum to discuss anything related to
39
40 Category:
41 Lounge:
42 title: The CodeIgniter Lounge
49 Thread_1:
50 title: Hi there!
51 Forum: Forum_1
52 Thread_2:
55
56 Post:
57 Post_1:
58 Thread: Thread_1
59 User: Test
60 created_at: 2009-11-20 01:20:30
61 content: >
62 Hello everyone! My name is Test, and I go to school at
69 Post_2:
70 Thread: Thread_1
71 User: Admin
72 created_at: 2009-11-20 02:15:33
75 Thread: Thread_2
76 User: Foo
Now we are going add something similar, but simpler, to our Message Board.
Home View
• Edit: system/application/views/home.php
view source
print?
01 <!DOCTYPE html>
02 <html lang="en">
03 <head>
04 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
05 <title>Home</title>
06 <link rel="stylesheet" href="<?php echo base_url(); ?>css/style.css"
07 type="text/css" media="all">
08 </head>
09 <body>
10
11 <div class="forums">
12
13 <div class="user_controls">
14 <?php if ($user = Current_User::user()): ?>
21 </div>
22
27 <div class="category">
28
33 <div class="forum">
34
35 <h3>
39
40 <div class="description">
43
44 </div>
45
46 <?php endforeach; ?>
47
48 </div>
49
50 <?php endforeach; ?>
51
52 </div>
53
54 </body>
55 </html>
We just added the highlighted lines.
Line 13: We are using the Current_User class, to see if the user is logged in. If they are logged
in, we will get an instance of the User Model. If not, it will return false.
Lines 15-16: We display the username, and link to the Logout Controller.
Lines 18-19: This section is for people who are not logged in. We link to the Login Controller
and Signup Controller.
So we pretty much have just utilized Models and Controllers we have built before.
We need to make a small addition to the css.
• Edit: css/style.css
• Add this to the end of the file.
view source
print?
1 /* USER CONTROL BOX -------------------------------*/
2 .user_controls {
3 float: right;
4 text-align: right;
5}
Testing
• Go to: http://localhost/ci_doctrine/
The links are showing up at the top right corner.
• Click Login.
• Login as Testuser:mypass .
Now we can see the username and the Logout link.
We will add more features to this later in other tutorials.
Profiling with Doctrine
Using Doctrine, our code is cleaner, smaller and easier to read and maintain. But it is sometimes
a good idea to look at a few things that are going on behind the scenes.
For example, Doctrine executes several SQL queries for us, while we use our Models. It would
be nice to know what these queries are, so we can see if there is more room for optimization.
This can be accomplished by Profiling.
For this, we are going to be using a Doctrine component called Doctrine_Connection_Profiler.
Profiling the Home Page
Let’s look at what queries are executed on the home page. We are going to make a quick and
dirty test for now. Later in the article we’ll make a more re-usable version.
There is going to be lots of new code here, but I will explain them line by line:
• Edit: system/application/controllers/test.php
view source
print?
01 <?php
02 class Test extends Controller {
03
04 //...
05
06 function home_profiler() {
07
08 // set up the profiler
11 $conn->setListener($profiler);
12 }
13
14 // copied from home controller
15 $vars['categories'] = Doctrine::getTable('Category')->findAll();
16
17 $this->load->view('home', $vars);
18
21 $events = array();
foreach ($profiler as $event)
22
{
23 $time += $event->getElapsedSecs();
if ($event->getName() == 'query' || $event->getName() ==
24
'execute') {
25 $event_details = array(
26 "type" => $event->getName(),
29 );
30 if (count($event->getParams())) {
31 $event_details["params"] = $event->getParams();
32 }
35 }
36 print_r($events);
39 }
40
41 }
Lines 15-17 are just copied from the home Controller. Before that, we set up the Profiler, and
after that we look at the data gathered by the Profiler.
Lines 9-12: First we create a Profiler object. Then we attach it to every Doctrine connection. In
our case, it will be just one connection. Now it’s ready to listen and record all Doctrine events.
Line 22: The Profiler object let’s us loop through it and get each Event object. Events can be
executed queries and also other things like fetching row data etc.. But we are only going to look
at query events.
Lines 20,23 and 37: Each Event has information on how much time it took. So we will add them
up in $time variable and display it at the end.
Line 24: We are only interested in SQL queries. These can be types ‘query’ or ‘execute’. If a
query has assigned parameters (for example ‘WHERE id = ?’), then it is of type ‘execute’,
otherwise it is ‘query’.
Lines 25-32: We create an array ($event_details) with information about the query. The type, the
query SQL, and the time it took to run. Also if there were any parameters, we add that too.
Lines 33 and 36: We add it all into the $events array, and just dump it at the end with print_r().
Line 38: We use the memory_get_peak_usage() PHP function to find out what the maximum
memory usage was during the script execution.
• Go to: http://localhost/ci_doctrine/test/home_profiler
• View Source:
Array
(
[0] => Array
(
[type] => query
[query] => SELECT c.id AS c__id, c.title AS c__title FROM
category c
[time] => 0.000220
)
)
)
03 /*
04 |--------------------------------------------------------------------------
07 |
08 | If you would like to use the "hooks" feature you must enable it by
09 | setting this variable to TRUE (boolean). See the user guide for details.
10 |
11 */
12 $config['enable_hooks'] = TRUE;
13
14 // ...
Just changed it from FALSE to TRUE. Now we can use Hooks.
Creating a Profiler Hook
Let’s create the functions that will be called for this hook.
• Create: system/application/hooks/doctrine_profiler_hooks.php
view source
print?
01 <?php
02 class Doctrine_Profiler_Hooks {
03
04 public static $profiler;
05
06 public function profiler_start() {
07
08 self::$profiler = new Doctrine_Connection_Profiler();
11 }
12
13 }
14
19 $events = array();
foreach (self::$profiler as $event)
20
{
21 $time += $event->getElapsedSecs();
if ($event->getName() == 'query' || $event->getName() ==
22
'execute') {
23 $event_details = array(
24 "type" => $event->getName(),
27 );
28 if (count($event->getParams())) {
29 $event_details["params"] = $event->getParams();
30 }
33 }
34
39
40 file_put_contents(BASEPATH."/logs/doctrine_profiler.php", $output);
41 }
42 }
The code is mostly the same as before. This time we are storing the $profiler as a static variable
in this class. Also, we are writing the output to a log file instead of dumping it into the browser.
Adding the Hooks
Now we need to add the hooks to the config so they get executed.
• Edit: system/application/config/hooks.php
view source
print?
01 <?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
02 /*
03 | -------------------------------------------------------------------------
04 | Hooks
05 | -------------------------------------------------------------------------
06 | This file lets you define "hooks" to extend CI without hacking the core
09 | http://codeigniter.com/user_guide/general/hooks.html
10 |
11 */
12
13 $hook['post_controller_constructor'][] = array(
14 'class' => 'Doctrine_Profiler_Hooks',
19
20 $hook['post_controller'][] = array(
'class' =>
21
'Doctrine_Profiler_Hooks',
22 'function' => 'profiler_end',
23 'filename' => 'doctrine_profiler_hooks.php',
24 'filepath' => 'hooks',
25 );
26
003 Array
004 (
013 (
014 [type] => execute
021
022 )
023
024 [2] => Array
025 (
026 [type] => execute
035
036 [3] => Array
037 (
038 [type] => execute
045
046 )
047
048 [4] => Array
049 (
050 [type] => execute
059
060 [5] => Array
061 (
062 [type] => execute
069
070 )
071
072 [6] => Array
073 (
074 [type] => execute
083
084 [7] => Array
085 (
086 [type] => execute
093
094 )
095
096 [8] => Array
097 (
098 [type] => execute
105
106 )
107
108 )
109
110 Total Doctrine time: 0.013517618179321
03
04 public function index() {
05
06 // $vars['categories'] = Doctrine::getTable('Category')->findAll();
07 $vars['categories'] = Doctrine_Query::create()
08 ->select('c.title, f.title, f.description')
11
12 $this->load->view('home', $vars);
13 }
14
15 }
We just commented out the findAll() line, and added a DQL call instead.
In the select() call we specify which fields we want to fetch.
In the from() call, we put the Models and use short aliases for them (c and f). Note that, we used
c.Forums instead c.Forum, even though the Model was named Forum. This is because we are
joining two models here, and in the Category Model definition, we referred to the Forum model
as Forums:
view source
print?
01 <?php
02
03 // models/category.php
04
09 }
10
15 ));
16 }
17 }
It was just more suitable to name in plural since it’s a one-to-many relationship.
Another thing to note is that we did not specify an ON clause in DQL. Normally when you join
tables in SQL, you would specify the columns you are joining on. But since our model
definitions know how these relationships are set up, ON clause is not necessary in this case.
Finally, we run execute() to fetch the Doctrine_Collection object.
Let’s load the home page again, and then look at the profiler output.
• First go to: http://localhost/ci_doctrine/
• Then open: system/logs/doctrine_profiler.php
You should see something like this:
view source
print?
<?php if ( ! defined('BASEPATH')) exit('No direct script access
01
allowed'); ?>
02
03 Array
04 (
11
12 [1] => Array
13 (
14 [type] => execute
19 [0] => 2
20 )
21
22 )
23
24 [2] => Array
25 (
26 [type] => execute
31 [0] => 1
32 )
33
34 )
35
36 [3] => Array
37 (
38 [type] => execute
43 [0] => 2
44 )
45
46 )
47
48 [4] => Array
49 (
50 [type] => execute
55 [0] => 3
56 )
57
58 )
59
60 [5] => Array
61 (
62 [type] => execute
67 [0] => 4
68 )
69
70 )
71
72 [6] => Array
73 (
74 [type] => execute
79 [0] => 5
80 )
81
82 )
83
84 )
85
86 Total Doctrine time: 0.012369155883789
03
04 public function index() {
05
06 // $vars['categories'] = Doctrine::getTable('Category')->findAll();
07 $vars['categories'] = Doctrine_Query::create()
08 ->select('c.title, f.title, f.description')
11 ->leftJoin('f.Threads t')
12 ->groupBy('f.id')
13 ->execute();
14 $this->load->view('home', $vars);
15 }
16
17 }
Highlighted lines are where the changes are.
Line 11: Now we are selecting from the Threads Model too, so we can count them. I could have
added the ‘Threads’ model into the from() call in Line 10. But I wanted to demonstrate another
way you can do joins in DQL. There is also innerjoin() if you need it.
Line 9: COUNT(t.id) will give us the number of threads. I could have added this into the select()
in Line 8, but for demonstration purposes again, I used the addSelect() function.
Line 12: Since we want individual Forums returned, and also are counting the Threads per
Forum, we are grouping the results by the Forum id field.
If you wanted to do the same thing in raw SQL, the query would look like this:
SELECT c.id, c.title, f.id, f.title, f.description, COUNT(t.id) AS
num_threads FROM category c LEFT JOIN forum f ON c.id = f.category_id LEFT
JOIN thread t ON f.id = t.forum_id GROUP BY f.id
One More Change
Now we need to do one more change. Since we are going to use the returned ‘num_threads’
value instead of calling $forum->Threads->count(), we need to change a line in the Home View.
• Edit: system/application/views/home.php around line #37
• Just change the highlighted line.
view source
print?
01 <!-- -->
02 <div class="forum">
03
04 <h3>
07 </h3>
08
09 <div class="description">
10 <?php echo $forum->description; ?>
11 </div>
12
13 </div>
14 <!-- -->
At the first glance, the structure might seem a bit odd. Why not ‘$forum->Threads-
>num_threads’? or even ‘$forum->num_threads’?
This has to do with the way the query is structured. We called COUNT() on the Threads.id
field. Therefore the returned data belongs to the Threads relationship, and not to the $forum
object directly.
Also, the relationship is one-to-many. Therefore $forum->Threads is a Doctrine_Collection by
default, instead of a Doctrine_Record. So we treat it like an array, and add the [0] index first
before we can get the data in num_threads.
The result is: $forum->Threads[0]->num_threads
If you are ever unsure about the structure of the returned Doctrine_Collection object, you can
convert it to an array using toArray(true) and dump it on the screen. Passing ‘true’ makes it
‘deep’, otherwise you only get the outermost object.
For example, if you do this:
view source
print?
1 $vars['categories'] = Doctrine_Query::create()
2 ->select('c.title, f.title, f.description')
3 ->addSelect('COUNT(t.id) as num_threads')
4 ->from('Category c, c.Forums f')
5 ->leftJoin('f.Threads t')
6 ->groupBy('f.id')
7 ->execute();
8 print_r($vars['categories']->toArray(true));
You can get an output like this:
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
[Category] =>
[Threads] => Array
(
[0] => Array
(
[id] =>
[title] =>
[forum_id] => 1
[Forum] =>
[num_threads] => 2
)
)
)
[category_id] => 1
[Category] =>
[Threads] => Array
(
[0] => Array
(
[id] =>
[title] =>
[forum_id] => 2
[Forum] =>
[num_threads] => 0
)
[num_threads] => 2
)
[category_id] => 2
[Category] =>
[Threads] => Array
(
[0] => Array
(
[id] =>
[title] =>
[forum_id] => 4
[Forum] =>
[num_threads] => 0
)
[category_id] => 2
[Category] =>
[Threads] => Array
(
[0] => Array
(
[id] =>
[title] =>
[forum_id] => 5
[Forum] =>
[num_threads] => 0
)
[num_threads] => 0
)
)
Profile Again
Let’s look at the profiling results again to see how the new DQL performed.
• First go to: http://localhost/ci_doctrine/
• Then open: system/logs/doctrine_profiler.php
You should see something like this:
view source
print?
<?php if ( ! defined('BASEPATH')) exit('No direct script access
01
allowed'); ?>
02
03 Array
04 (
11
12 [1] => Array
13 (
14 [type] => execute
19 [0] => 2
20 )
21
22 )
23
24 )
25
26 Total Doctrine time: 0.011896848678589
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
view source
print?
01 <!DOCTYPE html>
02 <html lang="en">
03 <head>
04 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
07 type="text/css" media="all">
08 </head>
09 <body>
10
13 <div class="user_controls">
14 <?php $this->load->view('user_controls'); ?>
15 </div>
16
21 </div>
22
23 </body>
24 </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
view source
print?
1 <?php if ($user = Current_User::user()): ?>
2 Hello, <em><?php echo $user->username; ?></em> <br/>
03
04 <h2><?php echo $category->title; ?></h2>
05
06 <?php foreach($category->Forums as $forum): ?>
07 <div class="forum">
08
09 <h3>
13
14 <div class="description">
17
18 </div>
21 </div>
22 <?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
view source
print?
01 <?php
02 class Home extends Controller {
03
04 public function index() {
05
06 $vars['categories'] = Doctrine_Query::create()
11 ->groupBy('f.id')
12 ->execute();
13
14 $vars['title'] = 'Home';
15 $vars['content_view'] = 'forum_list';
16 $vars['container_css'] = 'forums';
17
18 $this->load->view('template', $vars);
19 }
20
21 }
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
• Go to: http://localhost/ci_doctrine/
The page should look exactly the same as before:
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.
view source
print?
01 $category = Doctrine_Query::create()
02 ->select('c.*, f.*')
05 ->setHydrationMode(Doctrine::HYDRATE_SCALAR)
06 ->execute();
07
08 print_r($category);
09 /* output:
10 Array
11 (
12 [0] => Array
13 (
14 [c_id] => 1
19
20 [f_category_id] => 1
21 )
22
25 [c_id] => 1
26 [c_title] => The CodeIgniter Lounge
27 [f_id] => 2
28 [f_title] => The Lounge
31 [f_category_id] => 1
32 )
33
34 )
35 */
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:
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.
view source
print?
01 $category = Doctrine_Query::create()
02 ->select('c.*, f.*')
05 ->setHydrationMode(Doctrine::HYDRATE_ARRAY)
06 ->execute();
07
08 print_r($category);
09 /* output:
10 Array
11 (
12 [0] => Array
13 (
14 [id] => 1
17 (
18 [0] => Array
19 (
20 [id] => 1
23
24 [category_id] => 1
25 )
26
29 [id] => 2
30 [title] => The Lounge
33 [category_id] => 1
34 )
35
36 )
37
38 )
39
40 )
41 */
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
view source
print?
01 body {
02 font-family: "Trebuchet MS",Arial;
03 font-size: 14px;
04 background-color: #212426;
05 color: #B9AA81;
06 }
07 a {color: #FFF;}
08 a:hover {color: #B9AA81;}
11 }
12 .container {width: 720px; margin: auto;}
13 /* FORUMS -----------------------------------------*/
14 .forums.container h2 {
15 font-size: 16px;
16 color: #000;
19 background-color: #BBB;
20 -moz-border-radius-topleft: 6px;
21 -moz-border-radius-topright: 6px;
22 -webkit-border-top-left-radius: 6px;
23 -webkit-border-top-right-radius: 6px;
24 }
29 /* FORUM -----------------------------------------*/
30 .forum.container h2 {
31 font-size: 16px;
32 color: #000;
35 background-color: #BBB;
36 -moz-border-radius-topleft: 6px;
37 -moz-border-radius-topright: 6px;
38 -webkit-border-top-left-radius: 6px;
39 -webkit-border-top-right-radius: 6px;
40 }
45 #signup_form .heading {
46 text-align: center; font-size: 22px; font-weight: bold; color: #B9AA81;
47 }
48 #signup_form form {
49 background-color: #B9AA81;
50 padding: 10px;
51 -moz-border-radius: 8px;
52 -webkit-border-radius: 8px;
53 }
54 #signup_form form label {font-weight: bold; color: #11151E;}
57 font-weight: bold;
58 padding: 8px;
61 -webkit-border-radius: 4px;
62 }
65 margin: auto;
66 width: 200px;
67 font-size: 18px;
68 background-color: #FFF;
69 border: 1px solid #BBB;
70 }
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
view source
print?
1 <?php
2 class Forums extends Controller {
3
4 public function display($id) {
5
6 }
7
8}
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:
view source
print?
01 <?php
02 class Forums extends Controller {
03
04 public function display($id) {
05
06 $forum = Doctrine::getTable('Forum')->find($id);
07
08 $vars['title'] = $forum['title'];
09 $vars['threads'] = $forum->getThreadsArray();
10 $vars['content_view'] = 'forum';
11 $vars['container_css'] = 'forum';
12
13 $this->load->view('template', $vars);
14
15 }
16
17 }
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
view source
print?
01 <?php
02 class Forum extends Doctrine_Record {
03
04 // ...
05
06 public function getThreadsArray() {
07
08 $threads = Doctrine_Query::create()
09 ->select('t.title')
10 ->addSelect('p.id, (COUNT(p.id) - 1) as num_replies')
11 ->addSelect('MIN(p.id) as first_post_id')
12 ->from('Thread t, t.Posts p')
17
19
20 $post = Doctrine_Query::create()
21 ->select('p.created_at, u.username')
22 ->from('Post p, p.User u')
25 ->fetchOne();
26
27 $thread['num_replies'] = $thread['Posts'][0]['num_replies'];
28 $thread['created_at'] = $post['created_at'];
29 $thread['username'] = $post['User']['username'];
30 $thread['user_id'] = $post['User']['id'];
31 unset($thread['Posts']);
32
33 }
34
35 return $threads;
36
37 }
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
)
)
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
view source
print?
<h2><?php echo $title ?
01
></h2>
02
05
06 <h3>
09 </h3>
10
11 <div>
12 Author: <em><?php echo anchor('profile/display/'.$thread['user_id'],
13 $thread['username']); ?></em>,
14 Posted: <em><?php echo $thread['created_at']; ?></em>
15 </div>
16
17 </div>
18 <?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:
Seems to be working!
Day 10 – Pagination
Upgrading Doctrine
Before we get started, let’s upgrade Doctrine.
When I wrote the first article in this series, Doctrine 1.1 was the current stable version at the
time. Since then, 1.2 has come out. Luckily it is backwards compatible with 1.1.
So, if you are like me and have been using Doctrine 1.1, just follow these steps to upgrade to 1.2.
• Delete folder: system/application/plugins/doctrine/lib
• Download Doctrine 1.2 and extract.
• Copy the lib folder to system/application/plugins/doctrine
One Small Fix
At some point I did find an issue that wasn’t backwards compatible. I modified the previous
articles to fix that issue but some of you may not have done this if you read those articles before I
changed them.
• Edit: system/application/controllers/home.php
view source
print?
01 <?php
02 class Home extends Controller {
03
04 public function index() {
05
06 $vars['categories'] = Doctrine_Query::create()
11 ->groupBy('f.id')
12 ->execute();
13
14 $vars['title'] = 'Home';
15 $vars['content_view'] = 'forum_list';
16 $vars['container_css'] = 'forums';
17
18 $this->load->view('template', $vars);
19 }
20
21 }
It’s just the highlighted line. The ‘t.id’ needed to be added to the addselect() or COUNT(t.id) did
not work properly.
That is all. If your website is loading without errors, everything should be fine.
What is Pagination?
Displaying data in multiple pages is Pagination.
There are two main parts to this. First we need to generate links to the pages.
Pagination Links
Here is an example from CodeIgniter Forums:
That looks simple, but we also need to consider what happens when we start going to other
pages:
Now there is a “Prev” link which we didn’t have before. And we see links to 5 page numbers
instead of 3.
Things change even more if we go deeper:
Now there is also a “<< First" link, because the link for Page 1 is no longer visible.
One more:
The “Last >>” link disappeared because we already have a link to Page 89, which is the last
page.
Another example from Digg.com:
As you can see they are doing it quite differently. It’s all a matter of preference.
Let’s look at the way they do the links.
CodeIgniter Forums:
http://codeigniter.com/forums/viewforum/59/
http://codeigniter.com/forums/viewforum/59/P25/
http://codeigniter.com/forums/viewforum/59/P50/
Those were the links for the first, second and third page. The first page gets no parameter as it
works with default values. But the second and third pages have these “P25″ and “P50″
parameters at the end. Strangely enough these are not page numbers. They actually represent the
offset value for the records instead, as they display 25 records per page.
Digg.com:
http://digg.com/all/upcoming
http://digg.com/all/upcoming/page2
http://digg.com/all/upcoming/page3
Again, we see a difference between the two websites. Digg.com prefers to use actual page
numbers instead of offset numbers.
Paginating Data
This is the second part to this Pagination subject. Our code needs to be responsible for displaying
the correct set of data on each page.
In terms of raw SQL, here is how you can get a set of records for a given page:
SELECT * FROM table LIMIT 50, 25
The LIMIT clause does the job. In this particular query, we fetch 25 records, starting after the
first 50 records. So, 50 is the offset and 25 is the limit, i.e. the number of records.
With Doctrine we will be using DQL rather than raw SQL queries.
Let’s say we have a URL like they do on CodeIgniter forums:
http://codeigniter.com/forums/viewforum/59/P125/
That would translate into having the offset 125. And the limit would always be 25, because
internally we decide to display 25 threads per page.
If we have a Digg style pagination URL:
http://digg.com/all/upcoming/page7
We need to multiply 7 with the “per page” count. So it could be 175 (7*25) offset and 25 limit, if
the per page limit is set to 25. But we are not going to be using this method.
CodeIgniter Pagination Library
CodeIgniter comes with a nice simple Pagination Library, which we will be using. First, I will
explain it briefly.
Let’s put some code in a test Controller.
• Edit: system/application/controllers/test.php
view source
print?
01 class Test extends Controller {
02
03 function paginate() {
04
05 $this->load->library('pagination');
06
09 $config['per_page'] = '10';
10
11 $this->pagination->initialize($config);
12
13 echo $this->pagination->create_links();
14
15 }
16
17 }
The code is very simple. We just provide the base url, number of total records, and the number of
records per page.
The results look like this:
And the links look like this:
http://localhost/ci_doctrine/test/paginate/
http://localhost/ci_doctrine/test/paginate/10
http://localhost/ci_doctrine/test/paginate/20
http://localhost/ci_doctrine/test/paginate/30
http://localhost/ci_doctrine/test/paginate/40
So this library puts the offset number, rather than the page number at the end of the URL’s.
Also, with this library you can customize the HTML structure of the links. You can see more
information about that in the documentation.
Adding More Data
First we are going to add some more data to our database so we have something to paginate.
All we have to do is add more stuff to the fixture file we have been using before:
• Edit: system/fixtures/data.yml
view source
print?
001 User:
002 Admin:
015 Forum:
016 Forum_1:
021 Forum_2:
022 title: The Lounge
029 Forum_4:
030 title: Code and Application Development
039
040 Category:
041 Lounge:
042 title: The CodeIgniter Lounge
047
048 Thread:
049 Thread_1:
050 title: Hi there!
055 Thread_3:
056 title: Sed vitae ligula erat
057 Forum: Forum_3
058 Thread_4:
061 Thread_5:
062 title: Maecenas vel dolor odio
067 Thread_7:
068 title: Nullam volutpat laoreet orci
073 Thread_9:
074 title: Integer posuere luctus metus
079 Thread_11:
080 title: Pellentesque lacinia nibh vel lacus
081 Forum: Forum_3
082 Thread_12:
085 Thread_13:
086 title: Maecenas pretium nisi eget nunc rutrum quis
091 Thread_15:
092 title: Vivamus tempus semper libero
097 Thread_17:
098 title: Nam sagittis elementum turpis
103 Thread_19:
104 title: Praesent eget lorem nec odio
105 Forum: Forum_3
106 Thread_20:
109 Thread_21:
110 title: Ut sit amet ante nec leo volutpat
115 Thread_23:
116 title: Curabitur convallis sapien in dolor feugiat
121
122 Post:
123 Post_1:
124 Thread: Thread_1
131 I just found CodeIgniter some time last week and have been
132 reading through the documentation trying to get myself acquainted.
133
Hopefully the forums will be a great help! I already have some
134
questions.
135 Thanks!
136 Post_2:
195 content: Nunc non felis vitae dolor posuere aliquam non et augue.
196 Post_12:
225 content: Etiam tempor luctus sem, at consequat enim posuere in.
226 Post_17:
249 content: Donec eget lacus a nibh volutpat venenatis quis a felis.
250 Post_21:
content: Nullam sit amet purus nec mauris convallis tincidunt sodales
267
eget nunc.
268 Post_24:
273 content: Praesent in eros non elit ultricies tincidunt a nec tellus.
274 Post_25:
03 // ...
04
07 $threads = Doctrine_Query::create()
08 ->select('t.title')
11 ->addSelect('MAX(p.created_at) as last_post_date')
12 ->from('Thread t, t.Posts p')
15 ->orderBy('last_post_date DESC')
16 ->setHydrationMode(Doctrine::HYDRATE_ARRAY)
17 ->execute();
18
25 ->setHydrationMode(Doctrine::HYDRATE_ARRAY)
26 ->fetchOne();
27
28 $thread['num_replies'] = $thread['Posts'][0]['num_replies'];
29 $thread['created_at'] = $post['created_at'];
30 $thread['username'] = $post['User']['username'];
31 $thread['user_id'] = $post['User']['id'];
32 unset($thread['Posts']);
33
34 }
35
36 return $threads;
37
38 }
39
40 }
The highlighted lines have been added. They should be self-explanatory.
We should also update the View so it shows that date.
• Edit: system/application/views/forum.php
view source
print?
<h2><?php echo $title ?
01
></h2>
02
<?php foreach($threads as $thread): ?
03
>
04 <div class="thread">
05
06 <h3>
09 </h3>
10
11 <div>
12 Author: <em><?php echo anchor('profile/display/'.$thread['user_id'],
13 $thread['username']); ?></em>,
14 Last Post: <em><?php echo $thread['last_post_date']; ?></em>
15 </div>
16
17 </div>
18 <?php endforeach; ?>
Pagination Links
Now, to do pagination, we need 3 pieces of information:
• Total number of records. In our case, number of Threads.
• Number of records per page. We can pick any number. I will set it to 4 for
now so we can have several pages.
• Current page number or record offset. Since CodeIgniter Pagination
library works with the offset number, rather than page number, that’s what
we will use.
We need to edit the View, the Model and the Controller.
The View
Let’s set the output of the pagination links in the View.
• Edit: system/application/views/forum.php
view source
print?
01 <h2><?php echo $title ?
></h2>
02
05
06 <h3>
09 </h3>
10
11 <div>
12 Author: <em><?php echo anchor('profile/display/'.$thread['user_id'],
13 $thread['username']); ?></em>,
14 Last Post: <em><?php echo $thread['last_post_date']; ?></em>
15 </div>
16
17 </div>
18 <?php endforeach; ?>
19
20 <?php if (isset($pagination)): ?>
21 <div class="pagination">
22 Pages: <?php echo $pagination; ?>
23 </div>
24 <?php endif; ?>
At line 20, we check to see if the $pagination variable is set, which will contain the pagination
links, because sometimes there won’t be enough records to paginate.
The Model
Now let’s modify the Forum Model.
• Edit: system/application/models/forum.php
view source
print?
01 <?php
02 class Forum extends Doctrine_Record {
03
04 public function setTableDefinition() {
09
10 public function setUp() {
11 $this->hasOne('Category', array(
12 'local' => 'category_id',
19 }
20
21 public function numThreads() {
22
23 $result = Doctrine_Query::create()
24 ->select('COUNT(*) as num_threads')
25 ->from('Thread')
26 ->where('forum_id = ?', $this->id)
27 ->setHydrationMode(Doctrine::HYDRATE_ARRAY)
28 ->fetchOne();
29
30 return $result['num_threads'];
31
32 }
33
34 public function getThreadsArray($offset, $limit) {
35
36 $threads = Doctrine_Query::create()
37 ->select('t.title')
38 ->addSelect('p.id, (COUNT(p.id) - 1) as num_replies')
39 ->addSelect('MIN(p.id) as first_post_id')
40 ->addSelect('MAX(p.created_at) as last_post_date')
43 ->groupBy('t.id')
44 ->orderBy('last_post_date DESC')
45 ->limit($limit)
46 ->offset($offset)
47 ->setHydrationMode(Doctrine::HYDRATE_ARRAY)
48 ->execute();
49
51
52 $post = Doctrine_Query::create()
53 ->select('p.created_at, u.username')
54 ->from('Post p, p.User u')
57 ->fetchOne();
58
59 $thread['num_replies'] = $thread['Posts'][0]['num_replies'];
60 $thread['created_at'] = $post['created_at'];
61 $thread['username'] = $post['User']['username'];
62 $thread['user_id'] = $post['User']['id'];
63 unset($thread['Posts']);
64
65 }
66
67 return $threads;
68
69 }
70
71 }
Line 21: Added new method named “numThreads”, since we are going to need the total number
of Threads.
Note: I know that it would have been simpler to just use this code: “$this->Threads->count()”.
However, unfortunately Doctrine creates a very inefficient query when we do that, that’s why I
used a DQL query instead.
Line 34: Just added 2 parameters to the existing function, since we are only interested in the
Thread records for a given page.
Lines 45-46: Here we use the given $offset and $limit parameters to limit the DQL query results.
The Controller
Finally we update the Forums Controller.
• Edit: system/application/controllers/forums.php
view source
print?
01 <?php
02 class Forums extends Controller {
03
05
06 $per_page = 4;
07
08 $forum = Doctrine::getTable('Forum')->find($id);
09
10 $vars['title'] = $forum['title'];
11 $vars['threads'] = $forum->getThreadsArray(
12 $offset,
13 $per_page
14 );
15 $vars['content_view'] = 'forum';
16 $vars['container_css'] = 'forum';
17
18 $num_threads = $forum->numThreads();
19
20 // do we have enough to paginate
23 $this->load->library('pagination');
24 $config['base_url'] = base_url() . "forums/display/$id";
25 $config['total_rows'] = $num_threads;
26 $config['per_page'] = $per_page;
27 $config['uri_segment'] = 4;
28 $this->pagination->initialize($config);
29
30 $vars['pagination'] = $this->pagination->create_links();
31 }
32
33 $this->load->view('template', $vars);
34
35 }
36
37 }
Updated lines are highlighted.
Line 4: Now there is a second parameter named $offset, because the paginated URL’s will
contain the offset after the Forum id.
Line 6: Here we pick a number of Threads per page. Normally it could be 10, 20 or 25, but for
this test we are picking a smaller number.
Lines 12-13: We send the 2 parameters that the getThreadsArray method is expecting.
Line 21: We only do pagination if there are more records than the $per_page number.
Line 27: We have to set the ‘uri_segment’ setting for the Pagination library. This is the uri
segment that will carry the offset number. If not set, it defaults to uri segment 3. For example the
URL will look like this:
http://localhost/ci_doctrine/forums/display/53/8
“forums” is uri segment 1, “display” is uri segment 2, “53″ is uri segment 3 and the Forum id,
“8″ is the URI segment 4 and the $offset variable.
The Result
• Go to: http://localhost/ci_doctrine/
• Click the forum name: “CodeIgniter Discussion”
You should see:
Seems to be working
Day 11 – Record Hooks
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:
view source
print?
01 class Post extends Doctrine_Record {
02
05 }
06
09 }
10
15
16 }
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:
view source
print?
01 // from the Forum model
02
03 // ...
04 public function getThreadsArray($offset, $limit) {
05
06 $threads = Doctrine_Query::create()
07 ->select('t.title')
08 ->addSelect('p.id, (COUNT(p.id) - 1) as num_replies')
09 ->addSelect('MIN(p.id) as first_post_id')
10 ->addSelect('MAX(p.created_at) as last_post_date')
13 ->groupBy('t.id')
14 ->orderBy('last_post_date DESC')
15 ->limit($limit)
16 ->offset($offset)
17 ->setHydrationMode(Doctrine::HYDRATE_ARRAY)
18 ->execute();
19
21
22 $post = Doctrine_Query::create()
23 ->select('p.created_at, u.username')
24 ->from('Post p, p.User u')
27 ->fetchOne();
28
29 $thread['num_replies'] = $thread['Posts'][0]['num_replies'];
30 $thread['created_at'] = $post['created_at'];
31 $thread['username'] = $post['User']['username'];
32 $thread['user_id'] = $post['User']['id'];
33 unset($thread['Posts']);
34
35 }
36
37 return $threads;
38
39 }
40 // ...
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
view source
print?
01 <?php
02 class Thread extends Doctrine_Record {
03
04 public function setTableDefinition() {
09
10 public function setUp() {
11 $this->hasOne('Forum', array(
12 'local' => 'forum_id',
13 'foreign' => 'id'
14 ));
19
20 $this->hasOne('Post as First_Post', array(
23 ));
24 }
25
26 }
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
view source
print?
01 <?php
02 class Post extends Doctrine_Record {
03
04 // ...
09 $this['Thread']['First_Post'] = $this;
10 $this['Thread']->save();
11 }
12
13 }
14
15 }
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
view source
print?
01 <?php
02 class Forum extends Doctrine_Record {
03
04 // ...
05
06 public function getThreadsArray($offset, $limit) {
07
08 $threads = Doctrine_Query::create()
09 ->select('t.title')
10 ->addSelect('p.id, (COUNT(p.id) - 1) as num_replies')
11 ->addSelect('MAX(p.created_at) as last_post_date')
12 ->addSelect('fp.created_at, u.username')
15 ->groupBy('t.id')
16 ->orderBy('last_post_date DESC')
17 ->limit($limit)
18 ->offset($offset)
19 ->setHydrationMode(Doctrine::HYDRATE_ARRAY)
20 ->execute();
21
foreach ($threads as &$thread)
22
{
23
24 $thread['num_replies'] = $thread['Posts'][0]['num_replies'];
25 $thread['created_at'] = $thread['First_Post']['created_at'];
$thread['username'] = $thread['First_Post']['User']
26
['username'];
27 $thread['user_id'] = $thread['First_Post']['User']['id'];
28
29 unset($thread['Posts']);
30
31 }
32
33 return $threads;
34
35 }
36
37 }
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.