Doctrine Many To Many With Extra Fields
Recently I have started using Doctrine with Zend Framework. Most of the time it is great, but sometimes I get stuck on this or that issue. Most of my problems so far have been connected with the Many to Many relationship. Here are a few tips I learned the hard way.
Automatic relationship detection
When setting up a YAML file with your database schema you can start it with the handy declaration:
---
detect_relations: true
It does the One to Many relationship for you nicely, however the Many to Many ones did not work out of the box and required manual BaseModel tweaking. Take for example my Tag, Website, and WebsiteTag classes.
BaseTag:
abstract class BaseTag extends Doctrine_Record {
public function setTableDefinition() {
$this->setTableName('ts_tags');
$this->hasColumn('name', 'string', 128, array('type' => 'string', 'length' => '128'));
$this->hasColumn('is_category as isCategory', 'integer', 1, array('type' => 'integer', 'length' => '1'));
$this->hasColumn('page_id', 'integer', 8, array('type' => 'integer', 'length' => 8));
$this->hasColumn('ordering', 'integer', 5, array('type' => 'integer', 'length' => '5'));
}
public function setUp() {
$this->hasOne('Page', array('local' => 'page_id',
'foreign' => 'id'));
$this->hasMany('Website as Websites', array('local' => 'tag_id',
'foreign' => 'website_id',
'refClass' => 'WebsiteTag'));
$i18n0 = new Doctrine_Template_I18n(array('fields' => array(0 => 'name')));
$this->actAs($i18n0);
}
}
BaseWebsite:
abstract class BaseWebsite extends Doctrine_Record{
public function setTableDefinition() {
$this->setTableName('ts_websites');
$this->hasColumn('page_id', 'integer', 8, array('type' => 'integer', 'length' => 8));
$this->hasColumn('title', 'string', 128, array('type' => 'string', 'length' => '128'));
$this->hasColumn('client', 'string', 128, array('type' => 'string', 'length' => '128'));
$this->hasColumn('link', 'string', 255, array('type' => 'string', 'length' => '255'));
$this->hasColumn('intro', 'string', null, array('type' => 'string'));
$this->hasColumn('content', 'string', null, array('type' => 'string'));
$this->hasColumn('published', 'timestamp', null, array('type' => 'timestamp'));
$this->hasColumn('is_home as isHome', 'integer', 1, array('type' => 'integer', 'length' => '1'));
}
public function setUp() {
$this->hasOne('Page', array('local' => 'page_id',
'foreign' => 'id'));
$this->hasMany('Tag as Tags', array('local' => 'website_id',
'foreign' => 'tag_id',
'refClass' => 'WebsiteTag'));
$this->hasMany('Media', array('local' => 'website_id',
'foreign' => 'media_id',
'refClass' => 'WebsiteMedia'));
$i18n0 = new Doctrine_Template_I18n(array('fields' => array(0 => 'intro', 1 => 'content', 2 => 'title')));
$this->actAs($i18n0);
}
}
BaseWebsiteTag:
abstract class BaseWebsiteTag extends Doctrine_Record {
public function setTableDefinition() {
$this->setTableName('ts_website_has_tags');
$this->hasColumn('tag_id', 'integer', 8, array('type' => 'integer', 'length' => 8, 'primary' => true ));
$this->hasColumn('website_id', 'integer', 8, array('type' => 'integer', 'length' => 8, 'primary' => true));
}
public function setUp() {
}
}
The highlighted portions of code needed to be added. You can actually define these manually in the YAML file, the catch is that I was expecting it to happen magically with the auto relationship setting.
Many to Many with extra fields
Another little issue that Doctrine has is when you try to retrieve records. It in a way skips the joining table and retrieves a collection of the final items. In most cases this is exactly what you might need. However for my Media I decided to keep the ordering in the joining table – WebsiteMedia.
$q = Doctrine_Query::create()
->from('Website w')
->joinLeft('w.Media');
If you simply proceed with the above you only get Media objects without the ordering.
BaseWebsiteMedia:
abstract class BaseWebsiteMedia extends Doctrine_Record {
public function setTableDefinition() {
$this->setTableName('ts_website_has_media');
$this->hasColumn('website_id', 'integer', 8, array('type' => 'integer', 'length' => 8, 'primary' => true));
$this->hasColumn('media_id', 'integer', 8, array('type' => 'integer', 'length' => 8, 'primary' => true));
$this->hasColumn('caption', 'string', 255, array('type' => 'string', 'length' => '255'));
$this->hasColumn('is_main as isMain', 'integer', 1, array('type' => 'integer', 'length' => '1'));
$this->hasColumn('ordering', 'integer', 4, array('type' => 'integer', 'length' => '4'));
}
public function setUp() {
$this->hasOne('Website', array('local' => 'website_id',
'foreign' => 'id'));
$this->hasOne('Media', array('local' => 'media_id',
'foreign' => 'id'));
$i18n0 = new Doctrine_Template_I18n(array('fields' => array(0 => 'caption')));
$this->actAs($i18n0);
}
}
If you do not have the additional One to Many relations declared already you should add them now. With the modified model now you can get the records like so:
$q = Doctrine_Query::create()
->from('Website w')
->leftJoin('w.WebsiteMedia wm')
->leftJoin('wm.Media m');
This gives you a collection of WebsiteMedia records each containing the actual Media item.
You can leave a response, or trackback from your own site.

Thank you soooooooooo much man (or girld or ROBOT) !!! I added the missing relation to the auto-generated aux table, and my dql query works perfect.
:D:D
Very nice example. Would it be possible to have all Media items loaded for a Website even if the linking WebsiteMedia record doesn’t exist (should give null values on ordering etc.)? This might seem odd in the current situation, however try to visualize it using User -> User_Setting -> Setting tables. Where Setting has a default_value.
And what fixture file do you use for ts_website_has_media table?
Hi there,
Thank for this article.
I want to ask you just one question,
You have a many-to-many relationships between Media and Website.
The link-table have some property like “ordering”, “caption”…
The question is : “How do you proceed to order your result when you use the leftJoin on WebsiteMedia”.
Otherwise, How do you proceed to access on the properties of the link-table with DQL.
Thank,
Armetiz.
Not sure if that’s what you are asking, but as you are explicitly using WebsiteMedia (with the alias wm) you should be able to access the properties as say wm.ordering or wm.caption.
Don’t have the code at hand to check but I migth do a proper post about it later
Okay, I have not see the DQL correctly.
Two point are confused to me.
The first, I want to select only some properties. But the select () isn’t working when I’m using the link-table like this;
The second point is that you use a relation “WebsiteMedia” that isn’t define in the WebsiteBase::setUp () function.
An optional point is, that the
orderBy (“wm.ordring ASC”); doesn’t change the result order. Why..
$q = Doctrine_Query::create()
->from(‘Model_Db_Websites w’)
->leftJoin(‘w.WebsiteMedia wm’)
->leftJoin(‘wm.Media m’)
->orderBy(‘wm.ordering ASC’)
;
That’s the query above.
Inside WebsitesBase setUp():
$this->hasMany(‘Model_Db_WebsiteMedia as WebsiteMedia’, array(
‘local’ => ‘id’,
‘foreign’ => ‘websites_id’));
WebsiteMedia setUp() function:
$this->hasOne(‘Model_Db_Media as Media’, array(
‘local’ => ‘media_id’,
‘foreign’ => ‘id’));
$this->hasOne(‘Model_Db_Websites as Websites’, array(
‘local’ => ‘websites_id’,
‘foreign’ => ‘id’));
Do you how to define the fixture when the mapping (m:m) table has an extra field?
@Prasad Gupte
Text:
Text1:
title: ‘t
Author
Sorry for previous comment.
@Prasad Gupte
Text:
Text1:
title: ‘text1′
Text2:
title: ‘text2′
Author:
Author1:
name: ‘a1′
Author2:
name: ‘a2′
TextAuthor:
TextAuthor1:
author_id: Author1
text_id: Text1
TextAuthor2:
author_id: Author2
text_id: Text1
great post, thanks for sharing
I spent like 3 hours setting up the relations.. then I read your page and it hit me: The relations must be PROPERLY set up in BOTH of your Many-TO-Many Classes.. Not just your ‘connecting’ class.