Cookbook Recipe: Relation DQL Behavior
Posted on
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:
1 [yml]
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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
:
1 [yml]
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
**TIP** The ``autoComplete`` option is set to ``false`` so that the
relationship is not reflected and added to the opposite end,
``BlogPost`` automatically.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Now lets modify the BlogPost
model to change the Tags
relationship so that it is ordered alphabetically by name by default:
1 [yml]
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
**TIP** The ``relations`` array is an array of changes to make to
the DQL query. The key can be any valid function on the
``Doctrine_Query`` API and the value is of course the parameter to
pass to the function.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Now that we have our schemas modified to take into account the new RelationDql
behavior we need to actually write the code:
1 <?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));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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:
1 <?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;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
So now we have the behavior defined so lets look at some example DQL queries and the SQL that is outputted:
TIP Remember, in order for the dql callbacks to be executed we must enable an attribute first.
The above would output the following SQL:
Now lets look at an example that involves the BlogPost
tags:
The above would output the following SQL query:
1 [sql]
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
2
3
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.