User Dependent Forms with Symfony
Par NiKo le mercredi 17 février 2010, 09:04 - Dev
- Lien permanent -
11 commentaires -
Tags :
While sadly not being at Symfony Live 2010 with pals, I’m drowning my sorrow into tech tutorials writing.
Sometimes you want to design forms and contextualize them regarding the current user session. For example:
- Manage the display of some fields regarding the user authentication status and credentials;
- Filter some queries used to get the values available in a
<select/>tag; - Allow multiple steps form validation with step state persistence (wizzard);
- etc.
So how can we achieve this without using an evilish sfContext::getInstance()->getUser() call in the configure() method of the form?
Symfony 1.2 1.3 and 1.4 introduced the generation of a project-wide BaseForm class we can use here to setup some convenient methods to allow user setters and getters:
php
<?php
class BaseForm extends sfFormSymfony
{
static protected $user = null;
static public function getUser()
{
return self::$user;
}
static public function getValidUser()
{
if (!self::$user instanceof sfBasicSecurityUser)
{
throw new RuntimeException('No valid user instance available');
}
return self::$user;
}
static public function setUser(sfBasicSecurityUser $user)
{
self::$user = $user;
}
}
Okay, we now can set a user instance as a static property of all available forms within the project, nice. Wait, how the hell will we set the instance, and when?
Symfony dispatch a very useful context.load_factories event when all the factories - including the user one - are instanciated and available in the context. So we can listen to this event and set the user instance to the forms when it’s ready. We’ll do this in the ProjectConfiguration.class.php:
php
<?php
class ProjectConfiguration extends sfProjectConfiguration
{
public function setup()
{
// ...
$this->dispatcher->connect('context.load_factories', array($this, 'listenToLoadFactoriesEvent'));
}
public function listenToLoadFactoriesEvent(sfEvent $event)
{
BaseForm::setUser($event->getSubject()->getUser());
}
}
So now we’ve set a static user instance in every form at project configuration time, we can use it in any form.
For example, imagine a form where only authenticated users having an admin credential can see, fill and submit an hypothetic is_published field:
php
class myForm extends BaseForm
{
public function configure()
{
// will throw an exception if no user is available
$user = self::getValidUser();
$this->setWidgets(array(
'title' => new sfWidgetFormInputText(),
'body' => new sfWidgetFormTextarea(),
));
$this->setValidators(array(
'title' => new sfValidatorString(array('min_length' => 5)),
'body' => new sfValidatorString(array('min_length' => 20)),
));
if ($user->isAuthenticated() && $user->hasCredential('admin'))
{
$this->widgetSchema['is_published'] = new sfWidgetFormInputCheckbox();
$this->validatorSchema['is_published'] = new sfValidatorBoolean(array(
'required' => false,
));
}
// ...
}
}
Here a basic user won’t see and won’t be able to submit any value for the is_published field. Note that all those changes won’t ever affect the controllers or the model, and the forms will still be easily testable because the forms and the user instance are not tightly coupled.
As a side note, you can also use the form options to eventually pass a user instance and fallback on the static instance in case it’s not available:
php
class myForm extends BaseForm
{
public function configure()
{
// will throw an exception if no user is available
$user = $this->getOption('user', self::getValidUser());
// ...
}
}
In a controller, you would use it this way:
php
<?php
class fooActions extends sfActions
{
public function executeBar(sfWebRequest $request)
{
$this->form = new myForm(array('user' => $this->getUser()));
// ...
}
}
A typical unit test suite of the form would be:
php
$t = new lime_test(2, new lime_output_color());
$user = new sfBasicSecurityUser(new sfEventDispatcher(), new sfSessionTestStorage(array(
'session_path' => sys_get_temp_dir(),
)));
$form = new myForm(array('user' => $user));
$t->is(count($form->getWidgetSchema(), 2, '->configure() displays 2 fields when user is not authenticated'));
$user->setAuthenticated(true);
$user->addCredential('admin');
$t->is(count($form->getWidgetSchema(), 3, '->configure() displays 3 fields when user is authenticated and is an admin'));
// ... I'll let you complete the suite by yourself
Now imagine we want to filter the choices offered by a one to many relationship field of a Doctrine form regarding user credentials:
php
class ArticleForm extends BaseArticleForm
{
public function configure()
{
// will throw an exception if no user is available
$user = $this->getOption('user', self::getValidUser());
// ...
// Category choices
$categoryQuery = $this->getUserCategoryQuery($user);
$this->widgetSchema['category_id'] = new sfWidgetFormDoctrineChoice(array(
'model' => 'Category',
'query' => $categoryQuery,
));
$this->validatorSchema['community_id'] = new sfValidatorDoctrineChoice(array(
'model' => 'Category',
'query' => $categoryQuery,
));
}
/**
* Please note that this method would naturally better fit in the model, in the
* ArticleTable class; We put this here for conciseness
*/
protected function getUserCategoryQuery(sfBasicSecurityUser $user)
{
$query = Doctrine:getTable('Category')->createQuery('c');
// If user is not an admin, only allow choice of published categories
if (!$user->hasCredential('admin'))
{
$query->where('c.is_published = 1');
}
return $query;
}
}
There are many more cool things you can achieve by offering user session access to a form, while keeping all the stuff easy to test.
As usual, if you have a better way, feel free to tell us about it in the comments.
11 commentaires (Ajouter un commentaire)
Un grand merci pour cette astuce !
Je vais l'utiliser dans mes projets sf à la place du sfContext::getInstance()
Hey man! great job anyway.. it accomplishes perfectly the purpose. But it is a kind of hard working solution, I didn't find any "context.load_factories and ProjectConfiguration.class.php" issue documented anywhere. Million thanks!
BitCoder> That's the only way I found actually... Glad if it helped.
Thanks NiKo!
It saved me hours investigating how to make it work. Is there any other way to show only some options in a sfWidgetFormChoice according to a user attribute?
Best Regards,
Really great article, that is really what i looking for. thank you.
Olivier> Indeed... Fixed, thanks
as far I know, the project-wide BaseForm was introduced in sf 1.3.
cela va beaucoup nous aider à résoudre le ticket #5379 chez PMSIpilot : die sfContext::getInstance() die !!!!!!
Merci pour ce tuto' chouette méthode!!
Pratique aussi pour utiliser l'object i18ln dans configure()
dommage quand même que certains objet (comme user mais pourquoi pas d'autre selon la logique métier de l'application) ne soit pas accessible partout dans l'application sf.
Cela dit ce que tu propose est une façon plus complète du "classique"
<code>
$user = $this->getOption('user')
if(!(is_object($user) && $user instanceof sfBasicSecurityUser))
trhow new sfException('Form require a valid User');
</code>
s'ti veux, on peut organiser un sf Home 2011 l'année prochaine.... #sfHomeBiereCharentaiseJogging2011.
La discussion continue ailleurs
Social comments and analytics for this post
This post was mentioned on Twitter by weaverryan: RT @n1k0: [blog] User Dependant Forms with Symfony http://n1k.li/userforms...