Today I will teach you how to write a simple Doctrine behavior. You will learn some of the basics of creating a behavior and you will gain some pretty cool functionality for your relationships.
We will write a behavior called RelationDql which allows you to add default query parts that are automatically added to your queries when you reference the specified relationships. So first we will get started by looking at an example schema we can apply this to.
Here is an example schema where we have a Site, BlogPost and Tag model:
---
Site:
columns:
name: string(255)
BlogPost:
actAs: [Timestampable]
columns:
title: string(255)
body: clob
site_id: integer
relations:
Site:
foreignAlias: BlogPosts
Tags:
class: Tag
refClass: BlogPostTag
foreignAlias: BlogPosts
Tag:
columns:
name: string(255)
BlogPostTag:
columns:
blog_post_id:
type: integer
primary: true
tag_id:
type: integer
primary: true
relations:
BlogPost:
foreignAlias: BlogPostTags
Tag:
foreignAlias: BlogPostTags
This is a fairly simple schema as you can see, but what if we want to have a relationship on the Site model to retrieve the latest five BlogPost records or order the Tags relationship alphabetically by default?
Lets modify our schema to take into account a new behavior that we will write in the next step. First modify the Site model and a relationship named LatestBlogPosts:
---
Site:
actAs:
RelationDql:
relations:
LatestBlogPosts:
orderBy: %s.created_at DESC
limit: 5
columns:
name: string(255)
relations:
LatestBlogPosts:
autoComplete: false
class: BlogPost
local: id
foreign: site_id
The
autoCompleteoption is set tofalseso that the relationship is not reflected and added to the opposite end,BlogPostautomatically.
Now lets modify the BlogPost model to change the Tags relationship so that it is ordered alphabetically by name by default:
---
BlogPost:
actAs:
Timestampable:
RelationDql:
relations:
Tags:
orderBy: %s.name ASC
columns:
title: string(255)
body: clob
site_id: integer
relations:
Site:
foreignAlias: BlogPosts
Tags:
class: Tag
refClass: BlogPostTag
foreignAlias: BlogPosts
The
relationsarray is an array of changes to make to the DQL query. The key can be any valid function on theDoctrine_QueryAPI and the value is of course the parameter to pass to the function.
Now that we have our schemas modified to take into account the new RelationDql behavior we need to actually write the code:
<?php class RelationDql extends Doctrine_Template { protected $_options = array(); public function __construct($options) { $this->_options = $options; } public function setTableDefinition() { $this->_table->addRecordListener(new RelationDqlListener($this->_options)); } }
The template is very simple. It only attaches a record listener to the invoking table. Their is where most of the magic happens. So now lets define the RelationDqlListener class:
<?php class RelationDqlListener extends Doctrine_Record_Listener { protected $_options = array('relations' => array()); public function __construct($options) { $this->_options = $options; } public function preDqlSelect(Doctrine_Event $event) { $query = $event->getQuery(); if (empty($this->_options['relations'])) { throw new Doctrine_Exception( 'You must specify at least one relationship to add DQL to' ); } $relations = $this->_options['relations']; $components = $this->_getDqlCallbackComponents($query); foreach ($components as $alias => $component) { if (isset($component['relation']) && isset($relations[$component['relation']->getAlias()])) { $dqls = $relations[$component['relation']->getAlias()]; foreach ($dqls as $func => $dql) { $dql = str_replace('%s', $alias, $dql); $query->$func($dql); } unset($relations[$component['relation']->getAlias()]); } } } protected function _getDqlCallbackComponents($query) { $params = $query->getParams(); $componentsBefore = array(); if ($query->isSubquery()) { $componentsBefore = $query->getQueryComponents(); } $copy = $query->copy(); $copy->getSqlQuery($params); $componentsAfter = $copy->getQueryComponents(); if ($componentsBefore !== $componentsAfter) { return array_diff($componentsAfter, $componentsBefore); } else { return $componentsAfter; } } }
So now we have the behavior defined so lets look at some example DQL queries and the SQL that is outputted:
Remember, in order for the dql callbacks to be executed we must enable an attribute first.
<?php $manager->setAttribute('use_dql_callbacks', true);
<?php $q = Doctrine_Query::create() ->select('s.name, p.title, p.created_at') ->from('Site s') ->leftJoin('s.LatestBlogPosts p'); echo $q->getSql();
The above would output the following SQL:
SELECT
s.id AS s__id,
s.name AS s__name,
b.id AS b__id,
b.title AS b__title,
b.created_at AS b__created_at
FROM site s
LEFT JOIN blog_post b ON s.id = b.site_id
ORDER BY b.created_at DESC
LIMIT 5
Notice how the
ORDER BYandLIMITwere added to the query.
Now lets look at an example that involves the BlogPost tags:
<?php $q = Doctrine_Query::create() ->from('BlogPost p') ->leftJoin('p.Tags t'); echo $q->getSql();
The above would output the following SQL query:
SELECT
b.id AS b__id,
b.title AS b__title,
b.body AS b__body,
b.site_id AS b__site_id,
b.created_at AS b__created_at,
b.updated_at AS b__updated_at,
t.id AS t__id,
t.name AS t__name
FROM blog_post b
LEFT JOIN blog_post_tag b2 ON b.id = b2.blog_post_id
LEFT JOIN tag t ON t.id = b2.tag_id
ORDER BY t.name ASC
As you can see the ORDER BY clause to order the related tags by name was added for us.
Pretty cool huh? You can use this in your projects to make your relationships a little nicer.
Comments (13) [ add comment ]
Very cool Posted by pimpinken about about 1 year ago.
I like this a lot. I actually asked if this functionality was possible in the core at one point.
Thanks Posted by FX Poster about about 1 year ago.
Thank you very much! I've just thought about order-by in relations, but I couldn't even imagine, that this can be done through templates.
code listings in google reader Posted by Ariel Arjona about about 1 year ago.
great post
just wanted to make the observation that the code listings look messed up in google reader. The main problem is that it's not rendering the dark backgrounds.
Doesn't work Posted by FX Poster about about 1 year ago.
Doesn't work for me. Symfony 1.2, Doctrine, as I know, 1.0.6.
Some questions Posted by FX Poster about about 1 year ago.
Let's take an example:
For example, let's fetch something like that:
In resulting SQL еhere must be an "Order By"-clause for each join. In current implementation there is only one join in sql. But it can be simply fixed (by calling 'andOrderBy' instead of 'orderBy'). The main question is in the next example:
How can I make "->Children" to return ordered results? The resulting query doesn't match the 'Children' relation, so preDqlSelect doesn't do anything.
@FX Poster Posted by jwage about about 1 year ago.
I forgot to add a note that you must enable DQL callbacks for the exmple to work. I added a note to the blog post
You're right the above won't work, but you shouldn't ever be doing that :) You should always load your data through full DQL queries and avoid lazy loading data
I want more! Posted by FX Poster about about 1 year ago.
There are situations, where I already have an object and I need to fetch it's relations, just as I've shown before. Can it be done with some "monkeypatching" through symfony autoload system (replace Doctrine files with modified doctrine files)?
Loving it Posted by Vincent about about 1 year ago.
I just found out about Behaviours reading through the new book and I'm absolutely loving them, great work!
Loving it :) Posted by Vincent about about 1 year ago.
I just found out about Behaviours reading through the new book and I'm absolutely loving them, great work!
@jwage Posted by mh about 11 months ago.
{{{
$category = Doctrine::getTable('Category')->find(1); $category->Children; }}}
I know I shouldn't do something like this, but like it was said before, there are some situations I have to. How it could be solved in relatively simple way?
oops Posted by mh about 11 months ago.
looks like this does not work well in Doctrine 1.1...
Notice: Array to string conversion in ...\Doctrine\Doctrine \Connection\Statement.php on line 242
fixed? Posted by mh about 11 months ago.
dunno if I've done it right, but this seems to fix the problem:
in:
change:
to:
DQL callback enabled Posted by aglejberman about 10 days ago.
Don't know if this is a version issue, or what, but I was having problems to make this behavior to work.
I changed this: Doctrine_Manager::getInstance()->setAttribute('use_dql_callbacks', true);
for: Doctrine_Manager::getInstance()->setAttribute(Doctrine::ATTR_USE_DQL_CALLBACKS, true);
And everything went fine.
Hope this can help somebody. Alejandro.