PHP Doctrine introduction for dummies


It was long overdue but finally I’ve taken a look at Doctrine. And I’m blown away, bye bye Zend DB.

I’ve been whining about Zend Controller before, how it forces me to do things I don’t want to do. Therefore it’s my intention to simply write my own routing and I’m basically finished with the basics already but let’s focus on that in the next article because this will surely turn into a series 🙂 .

It’s time to try and convey how awesome I think Doctrine is, let’s start with the models that I’ve put in a subfolder with the same name, in Member.php:

class Member extends Doctrine_Record{
  public function setTableDefinition(){
    $this->hasColumn('username', 	'string',    30, array('minlength' => 6, 'regexp' => '/^\w+$/', 'unique' => true));
    $this->hasColumn('password', 	'string',    30, array('minlength' => 6));
    $this->hasColumn('email', 	      'string', 	30, array('notblank' => true, 'email' => true));
    $this->hasColumn('city', 		'integer', 	20);
  }
  
  public function setUp(){
    $this->actAs('Timestampable');
    $this->hasOne('City', 			 array('local' => 'city', 	'foreign' => 'id'));
    $this->hasMany('Message as From', array('local' => 'id', 	  'foreign' => 'from_id'));
    $this->hasMany('Message as To',    array('local' => 'id', 	   'foreign' => 'to_id'));
    $this->hasMany('Member as Senders', array('local' 	=> 'from_id', 
    											'foreign' 	=> 'to_id',
    											'refClass' 	=> 'Message'));
  	$this->hasMany('Member as Recipients', array('local' 	=> 'to_id', 
    											'foreign' 	=> 'from_id',
    											'refClass' 	=> 'Message'));
  }
}

So setTableDefinition() is filled with repeat calls to hasColumn() which basically defines our table. Notice the absence of ‘id’, it will be created automatically later when we use these definitions to actually create the tables in MySQL, this behavior mirrors Datamapper’s.

The actAs(‘Timestampable’) call will automatically add two columns that will keep track of when the object was created and last updated respectively.

The hasOne will setup a one directional relation, in this case a city whose model we’ll cover in a minute. HasMany() will setup all kinds of relationships which somehow involves more than one object on one side of the relation. In this case we have two one-to-many and many-to-many relationships. From the member to several messages, either sent or received and from the member to other members he might have sent and/or received messages from/to. Note the aliases (X as Y).

In Message.php:

class Message extends Doctrine_Record{
  public function setTableDefinition(){
    $this->hasColumn('subject', 'string', 	100);
    $this->hasColumn('body', 	'string', 	5000);
    $this->hasColumn('from_id', 'integer', 	20);
    $this->hasColumn('to_id',   'integer', 	20);
  }
  
  public function setUp(){
    $this->actAs('Timestampable');
    $this->hasOne('Member as From', array('local' => 'from_id', 'foreign' => 'id'));
    $this->hasOne('Member as To', array('local' => 'to_id',   'foreign' => 'id'));
  }
}

Nothing new except for the string column definition with a length of 5000, I noticed that that line will automatically create a column of type text.

City.php:

class City extends Doctrine_Record{
  public function setTableDefinition(){
    $this->hasColumn('name', 'string', 100);
  }
}

Below is a chopped up script from top to bottom that will test the functionality of the above models:

require_once('../lib/doctrine/lib/Doctrine.php');
spl_autoload_register(array('Doctrine','autoload'));
Doctrine_Manager::getInstance()->setAttribute('model_loading', 'conservative');
Doctrine::loadModels('models');
$conn = Doctrine_Manager::connection('mysql://root:@localhost/doctrine_test');

Basic bootstrap code, refreshingly little. We have to include the Doctrine.php script of course. Set the stuff to autoload, initiate conservative loading (i.e. load stuff only when needed), so loadModels(‘models’) will only parse the models directory, not load them. Finally we create the connection object. Note that in this case I’ve already created the database doctrine_test in phpMyAdmin even though it is possible to create databases with Doctrine too.

Doctrine::createTablesFromArray(array('Member', 'City', 'Message'));

$conn->beginTransaction();
$mbrs = array(
	array('member1', 'password1', 'member1@members.com', 'Berlin'),
	array('member2', 'password2', 'member2@members.com', 'Stuttgart'),
	array('member3', 'password3', 'member3@members.com', 'Hamburg')
);
foreach($mbrs as $m){
	$mbr = new Member();
	$mbr->username 		= $m[0];
	$mbr->password 		= $m[1];
	$mbr->email 		= $m[2];
	$mbr->City->name 	= $m[3];
	$mbr->save();
}
$conn->commit();

We setup the tables with the help of our models and create three members. That’s actually not completely true, note the $mbr->City->name line. Since the member hasOne City the line will automagically create a city too if one with the given name doesn’t already exist, great!

$tbl_mbr 	= Doctrine::getTable('Member');
$mbr1 		= $tbl_mbr->find(1);
$mbr2 		= $tbl_mbr->find(2);

$msg = new Message();
$msg->subject 	= 'from mbr1 to mbr2';
$msg->body 		= 'Hello mbr2 this is mbr1';
$msg->To 		= $mbr2;
$msg->From 		= $mbr1;
$msg->save();

$msg = new Message();
$msg->fromArray(array(
	'subject' 	=> 'from mbr2 to mbr1',
	'body'		=> 'Hello mbr1 this is mbr2',
	'from_id'	=> $mbr2->id,
	'to_id'		=> $mbr1->id
));
$msg->save();

$msg = new Message();
$msg->fromArray(array(
	'subject' 	=> 'from mbr1 to mbr3',
	'body'		=> 'Hello mbr3 this is mbr1',
	'from_id'	=> $mbr1->id,
	'to_id'		=> 3
));
$msg->save();
$conn->commit();

A demonstration of two different ways of doing things. We begin by creating two member objects for the members with id 1 and 2 with the help of a Doctrine::getTable(‘Member’) object. Since the message has a relation with two members in the form of To and From we can simply assign the objects directly and Doctrine will do the rest. The other way is to use an array with keys and values in a more old fashioned way that might be preferable in some situations. Finally we commit all our changes.

You can finish off with the following tests, they will all output the expected data:

$mbr = Doctrine::getTable('Member')->find(1);
print_r($mbr->Senders->toArray());
print_r($mbr->Recipients->toArray());
print_r($mbr->From->toArray());
print_r($mbr->To->toArray());

We get all people who has sent messages to the member in question through the Senders relation. Same goes for Recipients but now we get all people that member1 has sent messages to instead. And the two last From and To relations will get all messages that member1 has sent and received.

I’m not really sure how big impact all this magic has on performance, but it has to be pretty darn big for me to give up Doctrine and I hope the above has made you as excited as I am.

Related Posts

Tags: , , ,