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
Thanks very much for this wonderful post! The documentation of Doctrine isn't very clear about this, but my mind is now. :)
By the way: in your schema definition "userColumn" should be "userClass".
<A href="http://www.toptoys2trade.com/zhu-zh... ">zhu zhu pets</A>
<a href="http://www.toptoys2trade.com/a-sill... "> silly bandz</a>
<A href="http://www.toptoys2trade.com/power-... "> power balance</A>
<A href="http://www.toptoys2trade.com/animal... ">animal rubber bands</A>
<A href="http://www.toptoys2trade.com/baby-c... ">Baby Carriers</A>
<A href="http://www.toptoys2trade.com/pillow... ">Pillow Pets</A>
<A href="http://www.toptoys2trade.com/plush-... ">Plush Pencil Case</A>
<A href="http://www.golf-equipment2u.com ">golf equipments</A>
[url=http://www.b2chandbag.com/guess-han... ]guess handbags[/url]
[url=http://www.b2chandbag.com/d&g-h... ]d&g handbags[/url]
[url=http://www.b2chandbag.com/ ]prada handbags[/url]
[url=http://www.topcasualshoes.com/ ]ecco shoes[/url]
http://www.coachoutletfactory.com coach outlet
http://www.coachoutletfactory.com coach factory outlet
http://www.coachoutletfactory.com coach outlet factory
http://www.coachoutletfactory.com coach outlet online
http://www.coachoutletfactory.com coach outlet store
http://www.coachoutletfactory.com coach bags on sale
http://www.coachoutletfactory.com coach bags outlet
http://www.coachoutletfactory.com coach factory outlet online
http://www.coachoutletfactory.com coach outlet store online
http://www.coachoutletfactory.com coach outlet online store
http://www.coachoutletfactory.com coach factory outlet sale
http://www.coachoutletfactory.com coach online outlet store
http://www.coachoutletfactory.com/c... coach sunglasses
http://www.coachoutletfactory.com/c... COACH HANDBAGS
http://www.coachoutletfactory.com/c... COACH WALLETS
http://www.coachoutletfactory.com/c... COACH ACCESSORIES
http://www.coachoutletfactory.com/c... COACH BOOTS
http://www.coachoutletfactory.com/c... COACH SHOES
http://www.coachoutletfactory.com/c... Coach-Handbags
http://www.coachoutletfactory.com/c... COACH JEWELRY
http://www.coachoutletfactory.com/c... COACH APPAREL
http://www.coachoutletfactory.com/c... COACH MEN
http://www.coachoutletfactory.com/c... Coach Shoulder Bags
http://www.coachoutletfactory.com/c... Coach Sling Bags
http://www.coachoutletfactory.com/c... Coach Luggage Bags
http://www.coachoutletfactory.com/c... Coach Patchwork Purse
http://www.coachoutletfactory.com/c... Coach Baby Bags
http://www.coachoutletfactory.com/c... Coach Tote Bags
http://www.coachoutletfactory.com/c... Coach Backpack Bags
http://www.coachoutletfactory.com/c... Coach Carly Bags
http://www.coachoutletfactory.com/c... Coach Claire Bags
http://www.coachoutletfactory.com/c... Coach Garnet Bags
http://www.coachoutletfactory.com/c... Coach Hampton Bags
http://www.coachoutletfactory.com/c... Coach HOBO Bags
http://www.coachoutletfactory.com/c... Coach Leather Handbags
http://www.coachoutletfactory.com/c... Coach Sabrina Bags
http://www.coachoutletfactory.com/c... Coach Spotlight Bags
http://www.coachoutletfactory.com/c... Coach Travel Bags
http://www.coachoutletfactory.com/c... Coach Tribeca Bags
http://www.coachoutletfactory.com/c... New Coach Handbags
http://www.coachoutletfactory.com/c... Coach Ergo Bags
http://www.coachoutletfactory.com/c... Coach Maggie Bags
http://www.gucci-outlet.us/gucci outlet
http://www.gucci-outlet.us/gucci bags
http://www.gucci-outlet.us/gucci outlet online
http://www.gucci-outlet.us/gucci bags outlet
http://www.gucci-outlet.us/gucci-ha... Handbags
http://www.gucci-outlet.us/gucci-ha... Backpacks
http://www.gucci-outlet.us/gucci-ha... Belt Bags
http://www.gucci-outlet.us/gucci-ha... Briefcases
http://www.gucci-outlet.us/gucci-ha... Computer Cases
http://www.gucci-outlet.us/gucci-ha... Duffels
http://www.gucci-outlet.us/gucci-ha... Hobos
http://www.gucci-outlet.us/gucci-ha... Jolicoeur
http://www.gucci-outlet.us/gucci-ha... Messenger Bags
http://www.gucci-outlet.us/gucci-ha... Shoulder Bags
http://www.gucci-outlet.us/gucci-ha... Top Handles
http://www.gucci-outlet.us/gucci-ha... Totes
http://www.gucci-outlet.us/gucci-ha... Travel Business
http://www.gucci-outlet.us/gucci-le... Leather Wallets
http://www.gucci-outlet.us/gucci-su... Sunglasses
http://www.gucci-outlet.us/gucci-wa... Wallets
http://www.gucci-outlet.us/gucci-ha... Hats
http://www.gucci-outlet.us/gucci-be... belts
http://www.gucci-outlet.us/gucci-br... bracelets
http://www.gucci-outlet.us/gucci-ea... earrings
http://www.gucci-outlet.us/gucci-ne... necklaces
http://www.gucci-outlet.us/gucci-sh... shoes
http://www.gucci-outlet.us/gucci-ba... barrette
[url = http://www.airjordanforsale.com ]wholesale nike shox air max[/url]
[url = http://www.airjordanforsale.com ]wholesale air max 24/7[/url]
[url = http://www.airjordanforsale.com ]wholesale gucci shoes[/url]
[url = http://www.airjordanforsale.com ]wholesale docle gabbana t shirts[/url]
[url = http://www.airjordanforsale.com ]wholesale coach handbags[/url]
[url = http://www.airjordanforsale.com ]wholesale gucci clothes[/url]
[url = http://www.airjordanforsale.com ]wholesale ed hardy clothing[/url]
[url = http://www.airjordanforsale.com ]wholesale louis vuitton[/url]
[url = http://www.airjordanforsale.com ]wholesale nike air max shoes[/url]
[url = http://www.airjordanforsale.com ]wholesale gucci handbags[/url]
<a href="http://www.discountfromchina.com/ch...">discount Handbags & bags</a>
<a href="http://www.discountfromchina.com/ch...">wholesale sport jerseys</a>
<a href="http://www.giantsupplier.com/cheap-...">ed hardy sweatpants</a>
<a href="http://www.giantsupplier.com/fashio...">lacoste backpack</a>
<a href="http://www.wholesale-jerseys.net/NF...">wholesale nfl Jerseys</a>
<a href="http://www.wholesale-jerseys.net/NF...">wholesale Chicago Bears Jersey</a>
<a href="http://www.youngerfashion.com/belt-...">hot sell Clothing Accessories</a>
<a href="http://www.youngerfashion.com/nba-n...">cheap Jerseys</a>