How to create a custom Doctrine behavior
By eNk` on Thursday, November 26 2009, 15:19 - Symfony - Permalink
So all is in the tittle ^^, in this post I will speak about a way to create your own Doctrine behavior. I really want to share this because it's the first time I create a behavior and I didn't really find any post about it with some complete exemple with code.
Before starting:
Why behavior could be very useful ? I think this well explained in the Doctrine documentation here ^^.
Why I wrote this post ? The Doctrine documentation contains very good explainations about the way of using temaples but I think it's blur about the way you can link a template and a generator in order to generate some classes on the fly.
Let's go:
Here, the aim is to create a "Commentable" behavior to allow us to say that a class can have some comments and also create the corresponding comment class. In the exemple I suppose I need to add some comments on a Post class and a comment have the following fields: content, element_id, user_id and created_at. I would like to write my schema.yml like this: (and also be able to define the name of my user class to create a foreign key on the user_id field):
#config/doctrine/schema.yml
Post:
actAs:
Timestampable: ~
Commentable:
userColumn: MyUserClass
columns:
content: { type: string }
MyUserClass:
actAs:
Timestampable: ~
columns:
name: { type: string }
Let's start to create our behavior, first of all we need to create a template class to define fields and relations that a class on which we apply the template will have. In our exemple this template will be used on the Post class. So let's create the Commentable template to define that the Post class has many comments. To do this template simply create a new class (for exemple in my_project/lib/template/) and extend the Doctrine_Template class. We will have to use two functions:
- setTableDefinition(): define the table fields
- setUp(): define the relations and load generators.
// lib/template/Commentable.class.php
class Commentable extends Doctrine_Template
{
public function setTableDefinition() { }
public function setUp()
{
$this->hasMany($this->getTable()->getComponentName().'Comment as Comments', // here $this->getTable()->getComponentName() will return 'Post'
array('local' => 'id',
'foreign' => 'element_id'));
}
}
Ok so we have a temple to say that a commentable class has many comments, and now we need a comment class (it will be PostComment in the exemple), and we will also generate files for this class. So to do that we can create a generator. The generator class will define fields and relations for the comment class (PostComment) and generate the files if needed. Let's create a CommentGenerator class (in my_project/lib/generator/) which extend the Doctrine_Record_Generator class.
// lib/generator/CommentGenerator.class.php
class CommentGenerator extends Doctrine_Record_Generator
{
public function initOptions()
{
$builderOptions = array('suffix' => '.class.php',
'baseClassesDirectory' => 'base',
'generateBaseClasses' => true,
'generateTableClasses' => true,
'baseClassName' => 'sfDoctrineRecord');
$this->setOption('builderOptions', $builderOptions);
$this->setOption('className', '%CLASS%Comment');
$this->setOption('generateFiles', true);
$this->setOption('generatePath', sfConfig::get('sf_lib_dir').DIRECTORY_SEPARATOR.'model'.DIRECTORY_SEPARATOR.'doctrine');
}
public function getRelationLocalKey()
{
return 'element_id';
}
public function buildRelation()
{
$this->buildForeignRelation('Comments');
$this->buildLocalRelation('Element');
}
public function setTableDefinition()
{
$this->hasColumn('id', 'integer', null, array('primary' => true, 'autoincrement' => true));
$this->hasColumn('content', 'text', null, array('type' => 'text'));
$this->hasColumn($this->getRelationLocalKey(), 'integer');
$this->hasColumn('user_id', 'integer');
$this->hasColumn('created_at', 'date');
}
public function setUp()
{
$this->hasOne($this->getOption('userClass'),
array('local' => 'user_id',
'foreign' => 'id',
'onDelete' => 'cascade'));
}
}
Now we will add a listener to automaticly handle the created_at field on an insertion. Of course we could use the Timesptampable behavior in the table definition of CommentGenerator, but for the exemple I prefer create a new listener. So for this listener, we just need to create a CommentListener which extend from Doctrine_Record_Listener and overrride some function such as preInsert() in our case.
// lib/listener/CommentListener.class.php
class CommentListener extends Doctrine_Record_Listener
{
public function preInsert(Doctrine_Event $event)
{
$event->getInvoker()->created_at = date('Y-m-d', time());
}
}
Add then we can add the listener to our generator class in setTableDefinition(). Here I also add a constructor to get the options from the template .
// lib/generator/CommentGenerator.class.php
class CommentGenerator extends Doctrine_Record_Generator
{
public function __construct($options)
{
$this->addOptions($options);
}
public function initOptions()
{
$builderOptions = array('suffix' => '.class.php',
'baseClassesDirectory' => 'base',
'generateBaseClasses' => true,
'generateTableClasses' => true,
'baseClassName' => 'sfDoctrineRecord');
$this->setOption('builderOptions', $builderOptions);
$this->setOption('className', '%CLASS%Comment');
$this->setOption('generateFiles', true);
$this->setOption('generatePath', sfConfig::get('sf_lib_dir').DIRECTORY_SEPARATOR.'model'.DIRECTORY_SEPARATOR.'doctrine');
}
public function getRelationLocalKey()
{
return 'element_id';
}
public function buildRelation()
{
$this->buildForeignRelation('Comments');
$this->buildLocalRelation('Element');
}
public function setTableDefinition()
{
$this->hasColumn('id', 'integer', null, array('primary' => true, 'autoincrement' => true));
$this->hasColumn('content', 'text', null, array('type' => 'text'));
$this->hasColumn($this->getRelationLocalKey(), 'integer');
$this->hasColumn('user_id', 'integer');
$this->hasColumn('created', 'date');
$this->addListener(new CommentListener());
}
public function setUp()
{
$this->hasOne($this->getOption('userClass'),
array('local' => 'user_id',
'foreign' => 'id',
'onDelete' => 'cascade'));
}
// I override this function and set the generate_once option to true.
// The file seems to be generated each time the behavior is set up, so if you don't generate the file you don't need this, the class will be loaded in memory and nothing is write physically.
public function generateClassFromTable(Doctrine_Table $table)
{
$definition = array();
$definition['columns'] = $table->getColumns();
$definition['tableName'] = $table->getTableName();
$definition['actAs'] = $table->getTemplates();
$definition['generate_once'] = true;
return $this->generateClass($definition);
}
public function addOptions(array $options)
{
$this->_options = Doctrine_Lib::arrayDeepMerge($this->_options, $options);
}
}
So now in the Commentable template we just need to load the CommentGenerator as a template's plugin (the Doctrine_Template class also provide the loadGenerator() method).
// lib/template/Commentable.class.php
class Commentable extends Doctrine_Template
{
public function __construct(array $options = array())
{
parent::__construct($options);
if($this->getOption('userClass', null) == null)
{
throw new sfException('You need to specify the userClass option for Commentable.');
}
$this->_plugin = new CommentGenerator($this->getOptions());
}
public function setTableDefinition() { }
public function setUp()
{
$this->hasMany($this->getTable()->getComponentName().'Comment as Comments',
array('local' => 'id',
'foreign' => 'element_id'));
$this->_plugin->initialize($this->getTable());
}
}
Now just run a build-model and a build-sql and then look at the generated files and sql.
If you choose to generate files for PostComment you can also build forms and filters, and symfony you sould create the corresponding form and form filter classes.
Comments
Nice post ! It's very useful to can do this easily.
To do taggable, sortable, and now commentable :D
Other ideas of behaviors ?
This looks very nice. But how to use it? Could you provide an example?
I tried something similar to this:
(but this does not works)
$user = new MyUserClass();
$user->name = 'testuser';
$user->save();
$post = new Post();
$post->content = 'postcontent';
$comment = new PostComment();
$comment->content ="this is comment one";
$post->PostComments[] = $comment;
$post->save();
@Mariƫ: Hi :) in your exemple you create a PostComment but you don't set the element (post) and user ids. In this post I don't do anything to affect these values automaticaly. So you need to save the post object and then create the PostComment:
$comment = new PostComment();
$comment->content ="this is comment one";
$comment->setElementId($post->getId());
$comment->setUserId($user->getId());
$comment->save();
--
Update:
I just realize that there is a little mistake in my code, in the generator class. The relations for the XXXComment class are not correct. So I did a little change to fix it. I overrided getRelationLocalKey() and did some change in buildRelation() and setUp() in the CommentGenerator class.
Thanks!
I found this very useful. The Doctrine docs did not make generators very clear.
I'm currently trying to implement this using Zend framework instead of symfony. Could you explain the reason behind:
sfConfig::get('sf_lib_dir').DIRECTORY_SEPARATOR.'model'.DIRECTORY_SEPARATOR.'doctrine');
and how that could translate to Zend (if you're familiar? If not I can make that connection). Thanks!
@Pat:
I don't know ZF, but this
sfConfig::get('sf_lib_dir').DIRECTORY_SEPARATOR.'model'.DIRECTORY_SEPARATOR.'doctrine');
is the path to the folder where doctrine generate model classes (and Base* model classes)
Yeah okay, that's what I thought! I'll have to find a ZF equivalent to sfConfig::get I suppose :). Thanks!
I greatly appreciate all the info I've read here. I will spread the word about your blog to other people. Cheers.
have you tried generate migrations with this behaviour?
@Fizyk: no I haven't.
@eNk`: We've encountered some problems with migrations.
We're using diem for our projects, and with diem migrations are more than necessity (build --all is out of the question), and till symfony 1.4.4 you've got to run migrations-diff twice and clear cache between two runs to generate migrations (BaseToPrfx error), and slightly modify migration files.
Since symfony 1.4.4, you don't have to run the diff task twice, but still, you've got to check migration files, as each time diff task is being run,it creates migrations to drop all tables that were generated with behaviour. (and removes their models anyway)
Hi!
Have you tried to use i18n behavior in generated class, because it generates the translation stuff, but it makes a wrong base class in lib/model, like this:
public function setUp()
{
parent::setUp();
$doctrine_template_timestampable0 = new Doctrine_Template_Timestampable();
$doctrine_template_i18n0 = new Doctrine_Template_I18n();
$this->actAs($doctrine_template_timestampable0);
$this->actAs($doctrine_template_i18n0);
}
@ntomi: I tried quickly and I get the same result as you, but don't not why it doesn't work :(
Thanks for your shot, I will thinking on it
Do you want both quality and price, come here.
[url=http://www.toptoys2trade.com/animal... ]Animal Shaped Rubber Bands[/url],
[url=http://www.toptoys2trade.com/baby-c... ] Baby Carriers [/url],
[url=http://www.toptoys2trade.com/power-... ]power balance[/url],
[url=http://www.toptoys2trade.com/animal... ]Animal Rubber Bands[/url],
[url=http://www.toptoys2trade.com/mosqui... ]mosquito repellent wristband[/url]
[url=http://www.toptoys2trade.com/bakuga... ]new bakugan[/url],
[url=http://www.toptoys2trade.com/p90x-d... ]P90X DVDs[/url],
[url=http://www.toptoys2trade.com/zhu-zh... ] zhu zhu pets[/url],