Symfony2 Logo

Advance customization of the 403 error page in Symfony2

If you currently are testing the brand new Symfony2 framework, you might have tried to customize the 403 page. Actually, you have two documented ways to do so, first by overriding the default exception template, second by listening to the onCoreException event and filtering the right exception.

Unfortunately, neither of these two methods satisfied me. The simple customization of the template was not enough for my needs and the addition of an ExceptionListener impose a “maybe” to much complex filtering in order to retrieve just the 403 exception.

Note: Actually, my needs are similar to those I exposed in this blog post on symfony 1.4 : Symfony 1.4 vs. sfGuardUserPlugin : understand credentials and permissions.

What I want to do is to get the unsatisfied requirements and display them to the user.

First secure an action
In order to secure the access to my action with specific requirements, I use the @Secure annotation on my controller’s method. The annotation is provided by the JMSSecurityExtraBundle included in the symfony2 standard edition:

[php]
// /src/MyVendor/MyBundle/Controller/MyController.php

/**
* …
* @extra:Secure(roles="ROLE_MEMBER")
*/
public function newAction()
{
// …
}
[/php]

Here in order to access my action, the user will need to have the role named ROLE_MEMBER. If not, the security layer will throw an Exception resulting in a 403 response.

Change the unauthorized action
You can change the action that will display the 403 page in the security configuration:

[yml]
# /app/config/security.yml
security:
access_denied_url: /unauthorized
[/yml]

Now when dealing with an unauthorized Exception, the security layer will call the action associated to this URL. You need to specify the action that matches this URL in your routing configuration:

[yml]
# /src/MyVendor/MyBundle/Resources/config/routing.yml
unauthorized:
pattern: /unauthorized
defaults: { _controller: FrontendBundle:Default:unauthorized }
[/yml]

Now, the action responsible for rendering a 403 page is the following:

[php]
// /src/MyVendor/MyBundle/Controller/DefaultController.php

class DefaultController extends Controller
{
// …

/**
* @extra:Template()
*/
public function unauthorizedAction()
{
return array();
}
}
[/php]

And the associated template:

[html]
{# /src/MyVendor/MyBundle/Resources/views/Default/unauthorized.html.twig #}

403 error: Unauthorized

[/html]

Display the unmatched requirements
Now, the last things I want to do are to retrieve the required roles of the original request and display them to the user, so that he knows why his request did not succeed.
The tricky now is to obtain the original request, deduce the controller, load its annotations and thus deduce his requirements. Here is what I did:

[php]
// /src/MyVendor/MyBundle/Controller/DefaultController.php

public function unauthorizedAction()
{
// get the previous requests
$requests = $this->container->getCurrentScopedStack(‘request’);

// get the previous controller
$controllerResolver = new SymfonyComponentHttpKernelControllerControllerResolver();
list($controller, $method) = $controllerResolver->getController($requests['request']['request']);
// isolate the method
$method = new ReflectionMethod($controller, $method);
// load the metas
$reader = new JMSSecurityExtraBundleMappingDriverAnnotationReader();
$converter = new JMSSecurityExtraBundleMappingDriverAnnotationConverter();
$annotations = $reader->getMethodAnnotations($method);
$metadata = $converter->convertMethodAnnotations($method, $annotations)->getAsArray();
// isolate the required roles
$requiredRoles = $metadata['roles'];

return array(‘requiredRoles’ => $requiredRoles);
}
[/php]

The first thing I do is to obtain from the container the request stack that contains the original request. From the request I use the ControllerResolver in order to retrieve the initial controller and then isolate the method. With this method, I can use the AnnotationReader and AnnotationConverter from the JMSSecurityExtraBundle and get back the required roles.
Despite the fact that the method is pretty straightforward, it is not as elegant as I wanted it to be (in the whole process, the controller resolving and annotation extraction will be done twice). But it does the job.

The final thing to do is the show the required roles to the user in the template:

[html]
{# /src/MyVendor/MyBundle/Resources/views/Default/unauthorized.html.twig #}

Required roles:

{% for role in requiredRoles %}
{{ role }}

{% endfor %}

[/html]

symfony

Symfony 1.4 vs. sfGuardUserPlugin : understand credentials and permissions

I am currently dealing with a project that has strong requirements on how the user authorizations are given amount the users.
If you are familiar with the symphony 1.4 jobeet tutorial you should have seen first authorizations and then a quick introduction to the sfGuardUserPlugin. Let’s get around that first:
Credentials
They are a nice way to define borders to you application. You can place them on every action very easily by editing the security.yml file of the zone where you are setting your rights.
[yml]
all:
is_secure: false
new:
is_secure: true
credentials: manager
create:
is_secure: true
credentials: manager
[/yml]
Here I simply said that my module is accessible for everyone, except the new and create actions where the user needs to be logged and evermore he need to have the manager credential.
The action workflow checks the needed credentials just before loading the action, and if the user has it, it executes the actions, if not, a 403 page is given. The function of the user that is used is called hasCredential.
The first thing you have to know is that credentials are given to the sf_user, even if he is not logged. This means that thay are session dependent and cannot be persists amount sessions. Given credentials are not mean to be stored in your database.
Permissions
They came with the sfGuarduserPlugin. The plugin gives you the sfGuardSecurityUser, which extends the sf_user and which can be persisted. You can also give to him permissions or groups. Groups can have permissions in such a way that a user automatically gets the permissions of his group.
Moreover, the User, Groups and Permissions can be stored in your database.
[php]
// add a group
$sfGuardSecurityUser->addGroupByName(‘manager’);
// add permission
$sfGuardSecurityUser->addPermissionByName(‘manager’);
// get permissions (direct permissions and also group permissions)
$sfGuardSecurityUser->getAllPermissions();
[/php]
The matter now is that the controller workflow does not use the permissions in order to compare them to the needed credentials for an action.
How to check credentials using the permissions
If we want to combine the nice yaml credential description and the powerful permission system, we need to override the hasCredential method.
The cool thing here is that the instance of the user used in this case is the myUser class defined in your application folder. This way our can override the method in a non-obtrusive manner:
[php]
// /apps/frontend/lib/myUser.class.php
class myUser extends sfGuardSecurityUser {
public function hasCredential($credential, $useAnd = true) {
// combine the credential and the permission check
return (parent::hasCredential($credential, $useAnd) || parent::hasPermission($credential));
}
}
[/php]
Upgrade the 403 page
What I like to do sometimes is to tell the user what credentials he misses. In order to do that, we will need to change the secure function that redirects to the 403 page.
Go to the sfGuardPlugin and look for the sfGuardAuthActions.class.php file :
[php]
// /plugins/sfDoctrineGuardPlugin/modules/sfGuardAuth/actions/actions.class.php
class sfGuardAuthActions extends BasesfGuardAuthActions
{
public function executeSecure($request) {
parent::executeSecure($request);
// retrieve the needed credentials
$this->credentials = $this->getContext()->getActionStack()->getFirstEntry()->getActionInstance()->getCredential();
// make sure the credentials are an array (in case of unique non-array credential)
if(!is_array($this->credentials))
$this->credentials = array($this->credentials);
}
}
[/php]
You noticed here that I use the first controller called in the action stack. You have to do that in order to retrieve credentials of the first action that tried to reach the user. If not, you will only retrieve the credentials of the current controller which does not have any.
You now have the credentials that you can display in your template.
[php]
// /plugins/sfDoctrineGuardPlugin/modules/sfGuardAuth/tempates/secureSuccess.php
You have to meet the following requirements to have access to this action:
<ul>
<?php foreach($credentials as $credential): ?>
<li>
<?php switch ($credential){
case ‘manager’:
echo ‘You need to be a manager’;
break;
}?>
</li>
<?php endforeach;?>
</ul>
[/php]
And voila, you have now a nice and powerful user authorization management on your symphony website.

symfony

How to use filters on custom fields with symfony 1.4

I recently continued to use my particular relationship that embeds some properties. This time I wanted to add some filters to my student request using the pre-generated filter used in the backend.

Here again, is our example:

N to N relationship with properties

N to N relationship with properties

And the pre-coded filter is here:
[php]
// lib/filter/doctrine/StudentFormFilter.class.php
class StudentFormFilter extends BaseStudentFormFilter
{
public function configure()
{
}
}
[/php]

Here the filter already includes fields for every field on Student objects (id and name). It is a regular form, so you can override the predefined widgets and validator or even unset some of them. Let’s unset the id:

[php]
// lib/filter/doctrine/StudentFormFilter.class.php
class StudentFormFilter extends BaseStudentFormFilter
{
public function configure(){
unset($this['id']);
}
}
[/php]

Now we can use our filter like this in our controller:
[php]
// app/frontend/module/student/actions/actions.class.php
class studentActions extends sfActions {
public function executeIndex(sfWebRequest $request) {
//create a default query
$query = Doctrine::getTable(‘Student’)->getQuery();

//obtain the filter values that could have been submited
$filterValues = $request->getParameter(‘student_filters’);
//create the StudentFormFilter using the values, and giving it the query
$this->formFilter = new StudentFormFilter($filterValues);
$this->formFilter->setQuery($query);
//if the form have been submited
if($filterValues){
//bind the values
$this->formFilter->bind($filterValues);
//if valid, specialise the request using the form
if($this->formFilter->isValid())
$query = $this->formFilter->getQuery();
}

//finally get the students
$this->students = $query->execute();
}
}
[/php]

This code could seem a little weird but it is finally all you need to know in order to make a basic use of the auto generated filters. Thus, your template can look like this:

[php]
<?php echo form_tag(url_for(‘student_index’)) ; ?>
<table>
<?php echo $formFilter ; ?>
</table>
<input type="submit"/>
<?php echo ‘</form>’ ?>
// …
<ul>
<?php foreach($students as $student): ?>
<li><?php echo $student->getName(); ?></li>
<?php endforeach; ?>
</ul>
[/php]

Ok, everything is working quite well. But, let’s getting our filter trickier. I want to add a filter that permits me to select students that have a specific enrolment date.

First of all, we need to add a widget and a validator:
[php]
// lib/filter/doctrine/StudentFormFilter.class.php
class StudentFormFilter extends BaseStudentFormFilter
{
public function configure()
unset($this['id']);

$this->setWidget(‘EnrolmentDate’, new sfWidgetFormFilterDate( array(
‘from_date’ => new sfWidgetFormDate(),
‘to_date’ => new sfWidgetFormDate(),
‘with_empty’ => false)));
$this->setValidator(‘EnrolmentDate’, new sfValidatorDateRange(array(
‘required’ => false,
‘from_date’ => new sfValidatorDateTime(array(
‘required’ => false,
‘datetime_output’ => ‘Y-m-d 00:00:00′)
),
‘to_date’ => new sfValidatorDateTime(array(
‘required’ => false,
‘datetime_output’ => ‘Y-m-d 23:59:59′)))));
}
}
[/php]

Here, our form will display properly, but the getQuery method used in the will simply ignore our new EnrolmentDate field.

Now the things are getting really uncommon. Symfony is not really built for this kind of behavior, and we will need to override some behavior in our StudentFormFilter to get thing to work. In order to do that I read the sfFormFilterDOctrine.class.php file and more specifically the doBuildQuery function where the request is built. Some of the things I’ll do are a bit ugly but it is the price to pay if we want not to spread our modifications into the core framework, thus I managed to keep everything in the StudentFormFilter.class.php file. This way our ugliness remains contained into a single file and does not impact the framework. So let’s go for it!

First of all we need to add the EnrolmentDate as a new field of the form, to do that we need to override the getFields function:
[php]
// lib/filter/doctrine/StudentFormFilter.class.php
class StudentFormFilter extends BaseStudentFormFilter
{
// …

public function getFields() {
return array_merge(parrent::getFields(), array(‘EnrolmentDate’=>’EnrolmentDate’));
}
}
[/php]

The very important thing here is to know that I added a field EnrolmentDate of type EnrolmentDate. It will be crucial later.

Also, the doBuildQuery method will get the Student table and check for the presence of the field EnrolmentDate. The workaround here is to make a stub table and override the getTable function in order to return this stub table.

[php]
// lib/filter/doctrine/StudentFormFilter.class.php
class StudentFormFilter extends BaseStudentFormFilter
{
// …

public function getTable() {
return new StudentStubTable();
}
}

// my stub table
class StudentStubTable extends StudentTable{
public function hasField($field) {
if($field == ‘EnrolmentDate’)
return true;
else
return parent::hasField($field);
}
public function getFieldName($field){
if($field == ‘EnrolmentDate’)
return ‘EnrolmentDate’;
else
return parent::getFieldName($field);
}
}
[/php]

As you can see, two methods need to be overridden in my StudentStubTable class. Those two functions are used by the doBuildQuery function. Here i make the field EnrolmentDate exist and create his fieldName in the table is EnrolmentDate.

From here, all we need to know is that the doBuildQuery function will look for an addEnrolmentDateQuery function in our StudentFormFilter class. This function is called based on the type of the fields that I defined earlier in the getFields method. This call is made by the concatenation of ‘add’ + type of the field + ‘Query’.

So, let’s code the function and make it do our join with the Enrolment table.

[php]
// lib/filter/doctrine/StudentFormFilter.class.php
class StudentFormFilter extends BaseStudentFormFilter
{
// …

public function addEnrolmentDateQuery(Doctrine_Query $query, $field, $values) {
//add our join part
$query->leftJoin(sprintf(‘%s.Enrolment enrolment’,$query->getRootAlias()));

//set the where part depending on the value
if (isset($values['is_empty']) && $values['is_empty'])
{
$query->addWhere(‘enrolment.EnrolmentDate IS NULL’);
}
else
{
if (null !== $values['from'] && null !== $values['to'])
{
$query->andWhere(‘enrolment.EnrolmentDate >= ?’, $values['from']);
$query->andWhere(‘enrolment.EnrolmentDate <= ?’, $values['to']);
}
else if (null !== $values['from'])
{
$query->andWhere(‘enrolment.EnrolmentDate >= ?’, $values['from']);
}
else if (null !== $values['to'])
{
$query->andWhere(‘enrolment.EnrolmentDate <= ?’, $values['to']);
}
}
}
}
[/php]

In this method, we retrieve the query as a parameter and we just have to build our method based on the values we receive. Everything here is about building a proper DQL query.

Now we have a nice filter that can filter on our property embedded into a relationship.

Here is the final code:

[php]
// lib/filter/doctrine/StudentFormFilter.class.php
class StudentFormFilter extends BaseStudentFormFilter
{
public function configure()
unset($this['id']);

$this->setWidget(‘EnrolmentDate’, new sfWidgetFormFilterDate( array(
‘from_date’ => new sfWidgetFormDate(),
‘to_date’ => new sfWidgetFormDate(),
‘with_empty’ => false)));
$this->setValidator(‘EnrolmentDate’, new sfValidatorDateRange(array(
‘required’ => false,
‘from_date’ => new sfValidatorDateTime(array(
‘required’ => false,
‘datetime_output’ => ‘Y-m-d 00:00:00′)
),
‘to_date’ => new sfValidatorDateTime(array(
‘required’ => false,
‘datetime_output’ => ‘Y-m-d 23:59:59′)))));
}

public function addEnrolmentDateQuery(Doctrine_Query $query, $field, $values) {
//add our join part
$query->leftJoin(sprintf(‘%s.Enrolment enrolment’,$query->getRootAlias()));

//set the where part depending on the value
if (isset($values['is_empty']) && $values['is_empty'])
{
$query->addWhere(‘enrolment.EnrolmentDate IS NULL’);
}
else
{
if (null !== $values['from'] && null !== $values['to'])
{
$query->andWhere(‘enrolment.EnrolmentDate >= ?’, $values['from']);
$query->andWhere(‘enrolment.EnrolmentDate <= ?’, $values['to']);
}
else if (null !== $values['from'])
{
$query->andWhere(‘enrolment.EnrolmentDate >= ?’, $values['from']);
}
else if (null !== $values['to'])
{
$query->andWhere(‘enrolment.EnrolmentDate <= ?’, $values['to']);
}
}
}

public function getFields() {
return array_merge(parrent::getFields(), array(‘EnrolmentDate’=>’EnrolmentDate’));
}

public function getTable() {
return new StudentStubTable();
}
}

// my stub table
class StudentStubTable extends StudentTable{
public function hasField($field) {
if($field == ‘EnrolmentDate’)
return true;
else
return parent::hasField($field);
}
public function getFieldName($field){
if($field == ‘EnrolmentDate’)
return ‘EnrolmentDate’;
else
return parent::getFieldName($field);
}
}
[/php]

If you have any remarks about my implementation (I am sure you have) don’t hesitate to tell me about, I think this code can still be improved.

symfony

Adding actions to an auto generated admin in symfony 1.4

I know this functionality is nicely described in the symfony jobeet tutorial, but recalling this particular point emphasis the fact that symfony is definitely a good out of the box tool to easily generate powerful admin that should normally be very painful to code by hand.

In my example I will generate an admin for a system that deals with TV shows. The particular admin screen that I will customize is the TV Show list screen, in order to add it the “add season” action.
The “add season” action will simply add a season to my TV Show by retrieving the last season and increment the number and the year of the season.

Here is the UML for the TV Shows and the seasons:

UML of the tv show example

UML of the tv show example

We will first add the buttons to the admin interface, in order to do that I’ll just go to the generator.yml file and add the following lines:

[yml]
# /apps/backend/module/tvshow/config/generator.yml

generator:
class: sfDoctrineGenerator
param:
model_class: Tvshow
theme: admin
non_verbose_templates: true
with_show: false
singular: ~
plural: ~
route_prefix: tvshow
with_doctrine_route: true
actions_base_class: sfActions

config:
actions: ~
fields: ~
# customization of the list screen
list:
# header of the <table/>
display: [=Name]
# actions that will be in the combo box
batch_actions:
addSeason: ~
# actions that will be in the <table/>
object_actions:
addSeason: ~
_edit: ~
_delete: ~
filter: ~
form: ~
edit: ~
new: ~
[/yml]

You can note that the auto generated actions start with “_”, such as “_edit” and “_delete”. Moreover, the new “addSeason” action is automatically inserted in the screen and the routes and form submission are already working. You should obtain this:

Customized admin

Customized admin

Now we just need to code the actions that correspond to the route. Everything happens in the actions.calss.php dedicated to the tvshow generated module:

[php]
<?php

// /apps/backend/module/tvshow/actions/actions.class.php

require_once dirname(__FILE__).’/../lib/serieGeneratorConfiguration.class.php’;
require_once dirname(__FILE__).’/../lib/serieGeneratorHelper.class.php’;

class tvshowActions extends autoTvshowActions
{
/*
* Action executed when excecuting "addSeason" via the combo box
*/
public function executeBatchAddSeason(sfWebRequest $request)
{
// retrieving the ids correspondig to the checked boxes
$ids = $request->getParameter(‘ids’);

// query to retrieve the items via Doctrine
$q = Doctrine_Query::create()
->from(‘Tvshow t’)
->whereIn(‘t.id’, $ids);

// performing the addSeason on each TV show
foreach ($q->execute() as $tvshow)
$tvshow->addSeason();

// always inform the user
$this->getUser()->setFlash(‘notice’, ‘The selected TV Shows have been extended successfully.’);

// redirect to the auto generated module
$this->redirect(‘tvshow’);
}

/*
* Action executed when excecuting "addSeason" via the list directly
*/
public function executeListAddSeason(sfWebRequest $request)
{
// retrieving the tv show
$tvshow = $this->getRoute()->getObject();

// perform the addSeason
$tvshow->addSeason();

// always inform the user
$this->getUser()->setFlash(‘notice’, ‘The selected tv show have been extended successfully.’);

// redirect to the auto generated module
$this->redirect(‘tvshow’);
}
}
[/php]

Here we just miss the addSeason function:

[php]
<?php
// /lib/model/doctine/Tvshow.class.php

class Tvshow extends BaseTvshow {

/*
* Add a season the the TV show
*/
public function addSeason(){
// retrieve the last season via Doctrine
$last = Doctrine::getTable(‘Season’)
->createQuery()
->where(‘tvshowId=?’, $this->getId())
->orderBy(‘number DESC’)
->fetchOne();

// create the new one
$season = new Season();
$season->setTvshowId($this->getId());
$season->setYear($last->getYear() + 1);
$season->setNumber($last->getNumber() + 1);
$season->setNumberOfEpisodes($last->getNumberOfEpisodes());

// save it
$season->save();
}
}
[/php]

And everything is set. We now have a perfectly working new action included in our auto generated admin. This showing how symfony is highly customizable despite great functionalities in auto generated code.

Doctrine Logo

Doctrine 1.2: Add properties to a relationship

I recentry had the need to had properties to a relationship using Doctrine. In fact this use case is not taken care of by the ORM (and ORMs in general) and there is no really good solution. Nevertheless, I found a not so bad way to implement such a pattern.

Here is what I’ll do:

As you can see, a very common school example.

We can already describe our objects and the N to N relationship.

[yml]
# Shema.yml
Student:
columns:
id:
type: integer
primary: true
autoincrement: true
name:
type: string
relations:
Courses:
class: Course
local: student_id
foreign: course_id
foreignAlias: Students
refClass: Enrolment

Course:
columns:
id:
type: integer
primary: true
autoincrement: true
name:
type: string

Enrolment:
columns:
student_id:
type: integer
primary: true
course_id:
type: integer
primary: true
[/yml]

In can now load the Doctrine task Doctrine_Core::generateModelsFromYaml to generate my model.

A good way to see I the generation went well is to open the generated base files and read the class comments. Let’s have a look:

[php]
/**
* BaseCourse
*
* @property integer $id
* @property string $name
* @property Doctrine_Collection $Students
*/
abstract class BaseCourse extends Doctrine_Record
{
// …
}

/**
* BaseEnrolment
*
* @property integer $student_id
* @property integer $course_id
*/
abstract class BaseEnrolment extends Doctrine_Record
{
// …
}

/**
* BaseStudent
*
* @property integer $id
* @property string $name
* @property Doctrine_Collection $Courses
*/
abstract class BaseStudent extends Doctrine_Record
{
// …
}
[/php]

You can see that all my classes have been load with the right properties. The interestig part here is to see that the student and Course classes is agnostic from the Enrolment class. Thus the couse already has a collection of students and the student a collection of courses.

So we have here a perfectly working N to N relationship. Doctrine takes care it self of the join operation when accessing the database.

Now, let’s had our properties on the enrolment relationship.

The idea here is to provide Doctrine a way to acces the enrolment class. The most simplier way to do that is to add the following relationships to the enrolment definition:

[yml]
Enrolment:
columns:
student_id:
type: integer
primary: true
course_id:
type: integer
primary: true
enrolementDate:
type: date
notnull: true
relations:
Course:
local: course_id
foreign: id
foreignAlias: Enrolments
Student:
local: student_id
foreign: id
foreignAlias: Enrolments
[/yml]

You can notice that I added the One to N relationships and the enrolmentDate property.

After a class generation we obtain :

[php]
/**
* BaseCourse
*
* @property integer $id
* @property string $name
* @property Doctrine_Collection $Students
* @property Doctrine_Collection $Enrolments
*/
abstract class BaseCourse extends Doctrine_Record
{
// …
}

/**
* BaseEnrolment
*
* @property integer $student_id
* @property integer $course_id
* @property date $enrolmentDate
* @property Course $Course
* @property Student $Student
*/
abstract class BaseEnrolment extends Doctrine_Record
{
// …
}

/**
* BaseStudent
*
* @property integer $id
* @property string $name
* @property Doctrine_Collection $Courses
* @property Doctrine_Collection $Enrolments
*/
abstract class BaseStudent extends Doctrine_Record
{
// …
}
[/php]

I now have the additionnal relationships and I can access my relationship class which has a new property: enrolementDate.

But, such a schem force us to change some way of working with the model.

Working with the model

The first thing that we need to keep in mind, is that if we add a student to a course or a course to a student by using the Course::Students and the Student::Courses collections, Doctrine will autogenerate the Enrolement object and save it. Thus we won’t have access to the enrolementDate field.

So instead of using the common way of adding element to a collection we should instantiate a Enrolement object, associate it with the course and student, and then save it. During this process we so have access to the enrolementDate.

[php]
$student = new Student();
$student->name = "Michel";
$student->save();

$course = new Course();
$course->name = "Programming 101";
$course->save();

//create the relationship
$enrolment = new Enrolment();
$enrolment->enrolmentDate = date(‘c’);
$enrolment->Course = $course;
$enrolment->Student = $student;
$enrolment->save();
[/php]

After that, I have a correct set of data in my database. I can also do some access:

[php]
//retrieve the courses
$courses = $student->Courses;

//retrieve the enrolments
$enrolments = $student->Enrolments;

//retrieve the course passing through and enrolment
$course = $student->Enrolments[0]->Course;
[/php]

The only matter is that I cannot access an enrolment given a student and a course.

Here is what I did to pass this issue. I simply added a function to my Student class that will generate the

[php]
class Student extends BaseStudent
{
public function getEnrolmentForCourse($course){
return Doctrine::getTable(‘Enrolment’)
->findOneByCourseIdAndStudentId(
$course->id,
$this->id
);
}
}
[/php]

As you can see, I simply use a magic function of doctrine. Even if the findOneByCourseIdAndStudentId function does not exist, Doctrine uses its name to generate the appropriate SQL query; I just need to fill the blanks with the ids of both objects.

Now I have a complete access to my enrolment object:
[php]
// find Michel
$student = Doctrine::getTable(‘Student’)->findOneByName(‘Michel’);
// take his first course
$course = $student->Courses[0];

// retrieve the enrolment data from it
$enrolment = $student->getEnrolmentForCourse($course);
[/php]

We saw here how to implement a full access to a relationship containing properties. It involves some schema description and a simple additional method in the classes’ code, but it imposes a specific way to create N to N relations. Thus it is not perfect; I hope this solution will help you working better with Doctrine.