Symfony2 Logo

Symfony2: A translation message extractor command

One of the very painful taks you may face using symfony2 is the extration of all you translation message from your twig templates. This is much more annoying knowing that symfony1.4 did the job for you with a simple command, which does not exist in symfony2.

Today I will give give you a command for symfony2 that checks all your twig messages, combine them with your already existing messages in your yaml translations files and save the new ones. It is a recent work for me and it just works with twig/yml files.

The Command
I embedded it in a Bundle on github: https://github.com/michelsalib/ExtraToolsBundle.

You just need to get it, register the namespace and the bundle.

The name of the command is [cci]bcc:trans:update locale bundleName[/cci], where the [cci]locale[/cci] is the targeted locale (en, fr, es…) and the [cci]bundleName[/cci] is the name of the targeted bundle. You have several options:
– –dump-messages to display your final messages
– –force to update/write your translation files, it also perform a backup of the old ones
– –prefix=”…” if you want to change the prefix use for your new messages, by default [cci]__ [/cci] is used

Some example:
– To extract the messages of your bundle and display them in the console:
[cci]bcc:trans:update –dump-messages fr MyBundle[/cci]

- You can save them:
[cci]bcc:trans:update –force fr MyBundle[/cci]

- In another language:
[cci]bcc:trans:update –force en MyBundle[/cci]

- Or if you want to chaneg the prefix used to generate the new messages:
[cci]bcc:trans:update –force –prefix=’myprefix’ en MyBundle[/cci]

Behind the scene
The trick behind the code is how to properly crawl your twig templates.

First to get them:
[cc lang=”php”]
// get bundle directory
$foundBundle = $this->getApplication()->getKernel()->getBundle(‘MyBundle’);
// get twig templates
$finder = new Finder();
$files = $finder->files()->name(‘*.html.twig’)->in($foundBundle->getPath() . ‘/Resources/views/’);
// iterate over the files
foreach ($files as $file) {
$path = $file->getPathname();
// parse the files
}
[/cc]

Here you just need to use the kernel to retrieve the bundle data from a simple bundle name. When you have the bundle path, you can use the finder to get all the file ending by [cci].html.twig[/cci] in the relative [cci]/Resources/views/[/cci] directory.

You can now parse the twig files:
[cc lang=”php”]
$tree = $twig->parse($twig->tokenize(file_get_contents($path)));
[/cc]

Then you get a tree composed of [cci]Twig_Node[/cci]. The rest is just an algorithmic problem using recursion and type checking to find [cci]SymfonyBridgeTwigNodeTransNode[/cci] (for [cci]{% trans %}…{% end trans %} syntax[/cci]) and [cci]Twig_Node_Print[/cci] that contains trans filter (for [cci]{{ … | trans }}[/cci] syntax).

When you finally have all your messages, you might want to save them into yaml. For that you have two very simple static functions:
[cc lang=”php”]
// get a yaml file into a php array
$array = SymfonyComponentYamlYaml::load($path);
// transform an array into a yaml string
$yml = SymfonyComponentYamlYaml::dump($array);
[/cc]

Wrap up
I hope this command will help you building better symfony2 app, by automatizing some work. Don’t hesitate to report me bugs, suggestions or to fork me on github!

Symfony2 Logo

Programmatically authenticate the user in symfony2

I’ll just share here a quick snippet that serves to pragmatically authenticate the user in Symfony2.

It is very useful, especially after checking an email account using a token for example. In this case you might don’t want to ask again the password to your user and do the authentication for him.

What you need to do is to manually create the AuthenticationToken and give it to the security context:

// create the authentication token
$token = new UsernamePasswordToken(
	$user,
	null,
	'main',
	$user->getRoles());
// give it to the security context
$this->container->get('security.context')->setToken($token);

Note that the third parameter of the token constructor is the name of the security provider associated with your user found in your app/config/security.yaml file.

Symfony2 Logo

Create your own constraint validator in symfony2 : A Doctrine unique validator

The new symfony2 form framework is definitely one of my favorites. It is quite simple, and very powerful and straightforward. Moreover it is very extensible.

One of the most common tasks you need to perform when validating a form is to check the unity of a field in your database. Unfortunately, there is no build-in validator coupled with doctrine. Here is my implementation.

Create the unique validator

The first thing to do when implementing a new validator is to create the constraint class:
[cc lang=”php”]
namespace MyAppMyBundleValidator;

use SymfonyComponentValidatorConstraint;

class Unique extends Constraint
{
public $message = ‘This value is already used';
public $entity;
public $property;

public function validatedBy()
{
return ‘validator.unique';
}

public function requiredOptions()
{
return array(‘entity’, ‘property’);
}

public function targets()
{
return self::PROPERTY_CONSTRAINT;
}
}
[/cc]

As you can see, this definition is pretty simple. We define the error message and two more required options which are the Entity that doctrine with check and its property. The validatedBy() method returns the service’s name that is in charge to validate the constraint (we will declare it just after). Finally, the targets() function limits the use of the constraint only to class’ property.

Now that our constraint is set, let’s create the constraint’s validator:
[cc lang=”php”]
namespace MyAppMyBundleValidator;

use DoctrineORMEntityManager;
use SymfonyComponentValidatorConstraint;
use SymfonyComponentValidatorConstraintValidator;

class UniqueValidator extends ConstraintValidator
{
private $entityManager;

public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}

public function isValid($value, Constraint $constraint) {
// try to get one entity that matches the constraint
$user = $this->entityManager->getRepository($constraint->entity)
->findOneBy(array($constraint->property => $value));
// if there is already an entity
if($user != null){
// the constraint does not pass
$this->setMessage($constraint->message);
return false;
}
// the constraint passes
return true;
}
}
[/cc]

The main function here is isValid(). It checks the validity of the constraint and consequently returns true/false and may add some error message. Here our job is to check is the value is already used in the database. The simplest way to do that is to try to get one matching entity. If Doctrine finds an entity the constraints does not pass, if there is no entity, the value can be used and the constraint passes.

One thing to know is that the Unique constraint instance is pass to the isValid() function, so that we can get the entity et property values.

Finally, our UniqueValidator has a dependency to the Doctrine manager. We will need to declare it in our services:

[cc lang=”yaml”]
# MyApp/MyBundle/Resources/config/services.yml
parameters:
my.validator.unique.class: MyAppMyBundleValidatorUniqueValidator

services:
my.validator.unique:
class: %my.validator.unique.class%
arguments: [@doctrine.orm.entity_manager]
tags:
– { name: validator.constraint_validator, alias: validator.unique }
[/cc]

We defined here the way to instantiate our UniqueValidator with its dependencies (arguments line). The tag line is very important. The name specifies that it is load as a constraint validator, and the alias name is used to link the constraint validator to its constraint (remember the Unique::validatedBy() function).
One last thing is to load our services.yml file. We can do it in the dependency injection configuration :
[cc lang=”php”]
load(‘services.yml’);
}

public function getAlias() {
return ‘my';
}
}
[/cc]

The load() function is launched at the configuration of the container. Here we simple retrieve the services.yml file and load it to the current configuration.

That’s it you know have a working constraint that you can use to check the unity of a field at form’s submission. Don’t forget to set the entity and property values.

Support annotation

Actually I prefer setting my constraints using annotations. To do so, we will need some more work.

Let’s say we want to use our constraint like that:
[cc lang=”php”]
/**
* @orm:Entity
*/
class User{
/**
* @orm:Column(length=255, unique=”TRUE”)
* @myvalidation:Unique(entity=”MyBundle:User”, property=”username”)
*/
protected $username;

//…
}
[/cc]

It won’t work right away. This is because the myvalidation is a shortcut to a namespace (just like validation or orm). Because we did not declare this shortcut, symfony cannot find the Unique class and simply ignores it.

To declare the myvalidation shortcut you can do it directly in the app/config/config.yml. I chose to do it in the bundle because I don’t like to play so much with the app configuration. One of the benefits of bundle is that they decoupled the application, so we don’t want our general configuration to become dependent to our bundle.

To declare the shortcut in our bundle we can do it in the dependency injection configuration:
[cc lang=”php”]
load(‘services.yml’);

// get the existing registered namespaces for validator annotations
$namespaces = $container->getParameter(‘validator.annotations.namespaces’);
// add our namespace under the alias myvalidation
$namespaces[‘myvalidation’] = ‘MyApp\MyBundle\Validator\';
// save it
$container->setParameter(‘validator.annotations.namespaces’, $namespaces);
}

public function getAlias() {
return ‘my';
}
}
[/cc]

What we need to do is to retrieve the namespace aliases of the validators and then add our alias. Don’t forget the trailing backslashes.

And voila! Our Unique constraint works also with annotations.

I hope that this tutorial will help you to build new powerful constraints and share them to the community.

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.