Symfony forms – Flexible widgets based on user credentials (sfcontext is evil)

Background

This issue has come up many times in the symfony forum and on IRC, and whilst it seems like a fairly trivial one, it is important to discuss the best practice around it. Often we want to modify a form based on something outside the form’s scope, like a user’s credentials or the page they are on, or maybe some session values – lets take the example of a dropdown widget where admin users get to see a few more options.

The bad way

Everyone that has been using symfony for a while is aware of the context singleton. This magic sprite allows us to grab all sorts of information about the context we are in, including the user, session, request, view, and many more things. Whilst it certainly has its place, most of the time, however, we should avoid using it. The clue to the reason why is carefully disguised in the class name “Context”.

When we refer to a context in our code, we are locking ourselves in to the fact that the context must exist, so every time we use it we are basically saying that this class can now only be used in a symfony project with a fully initialised symfony stack hanging over it. This becomes a problem in things like unit tests, where you have to mock up a loaded context object with bells and whistles in order to test a simple function or class.

So this is what you might think of doing:

// In your form class
public function configure()
{
  $choices = array(1 => "something boring", 2 => "something dull");
 
  $currentUser = sfContext::getInstance()->getUser();
 
  if ($currentUser->isAuthenticated() && $currentUser->hasCredential("admin"))
  {
    $choices += array(5 => "something cool", 6 => "something leet");
  }
 
  // ....
}

The reason this is bad is that the forms framework is a standalone framework – it should be possible to pick up your form class and drop it into any project. It should also be possible to test it independently of symfony, without being tied in by sfContext. So what is the better way?

The dependency injection approach

You might read that and think “woah, this is getting complicated!” but we’re not talking about dependency injection containers here, we’re simply saying that you can make your form object depend on something to run. The thing it depends on should not be the context singleton, it should be the minimum thing that the form needs to operate correctly – which in this case, is a user object that supports credentials.

// In your actions.class.php
$this->form = new myForm(array(), array("currentUser" => $this->getUser()));
 
// In your form class
 
// ...
if ( !($this->getOption("currentUser") instanceof sfUser))
{
  throw new InvalidArgumentException("You must pass a user object as an option to this form!");
}

The test here is where we are making this form “dependent” on a user object. In this case we are insisting that the object is an instance of sfUser, which you may argue is tying us in to symfony again, but you could use any test here to ensure that the object will have the necessary functionality you need, maybe check for the existence of a “hasCredential()” method for example.

When writing a test for this form class, we now only need to instantiate a user object and load it with some credentials – much easier than doing the same thing and locking into a context singleton. There may be other times when this form could be useful in a lightweight environment, where you can get speedy access to a user object but don’t want the overhead of the symfony context – you might not think of one now, but it’s best to code this way and you’ll have less reasons to kick yourself further down the line.

The completed code, for our single widget form

// In your form class
public function configure()
{
  if (!($this->getOption("currentUser") instanceof sfUser))
  {
    throw new InvalidArgumentException("You must pass a user object as an option to this form!");
  }
  else
  {
    $currentUser = $this->getOption("currentUser");
  }
  $choices = array(1 => "something boring", 2 => "something dull");
 
  if ($currentUser->isAuthenticated() && $currentUser->hasCredential("admin"))
  {
    $choices += array(5 => "something cool", 6 => "something leet");
  }
 
  $this->widgetSchema['my_dropdown']    = new sfWidgetFormChoice(array("choices" => $choices));
  $this->validatorSchema['my_dropdown'] = new sfValidatorChoice(array("choices" => array_keys($choices)));
}

Wrap up

Think about this example the next time you think about modifying a query based on a user object, or session value in a peer class, or Doctrine table class… Maybe you should have passed a parameter there too? Every class and method you write, think about how you can reuse it, will it even be possible the way you have written it? If something simply must be coded in a symfony specific way, think about making a parent and a child class for the problem you are trying to solve. In the parent class, you can make things as generic as possible – so you can re-use that class to your heart’s content. In the child class, you can add the symfony specific code – kept to a minimum.

27 Comments so far

  1. Duane Gran on August 5th, 2009

    Very good instructions and a valid point you bring up. FYI.. I believe the variable in the last two lines should be $options rather than $choices.

  2. john on August 5th, 2009

    Another awesome tutorial!

    I promise I will never use sfContext::getInstance() again 🙂

    If you wanted to do the same thing in the admin generator would you have to override the generated action to insert the sfUser object into the form ?

    That’s 4 functions which is a bummer: new, create, edit and update.

  3. Michael on August 5th, 2009

    Great point explanation.

  4. Russ on August 5th, 2009

    @Duane – thanks for pointing that out 🙂 I’ve renamed the $options variable to $choices as it makes more sense.

    @John – Try not to use it again, but there are places where it is the only way! For example sfContext::getInstance()->getI18n() comes in very handy sometimes for translating stuff outside templates. I’m not sure about the admin forms, but do let us know what you find out!

    Update: Since symfony 1.3 it is possible to call $this->getContext() from most of the places where it is “acceptable” to use it, for example in actions when you need to get hold of an i18n object. So if you still need to use sfContext::getInstance() then you *know* that something is really wrong!

  5. Anton on August 6th, 2009

    Nice to see the real problem and the solution. Good job, Russ!

  6. Gareth on August 6th, 2009

    I take it the code in the last snippet it supposed to be in a function of some sort? If so, which one?

  7. Russ on August 6th, 2009

    @Gareth – Haha, yes you are right – it’s the form configure() method.

  8. Mark on August 6th, 2009

    You bring up very valid points but I still have a major objection.

    Out of all the forms I’ve ever made for symfony, which is a lot, I’ve NEVER had to take the form out of the symfony framework stack. In fact, most projects are finished for a client and won’t be worked on in the future. I don’t want a delay because I’m coding something that will never be needed.

    I do agree with you testing wise however.

  9. Russ on August 6th, 2009

    @Mark – I don’t understand why that is an “major objection”? Sure I also make a lot of forms that don’t get reused, but when it’s no more effort to code in a “best practice” manner, why not? It will help your coding generally – this isn’t just about form classes.

    If you are actually objecting, you are implying that my advice is bad – if so, please clarify 🙂

  10. Daniel on August 17th, 2009

    Good article!

    To answer an earlier question about modifying the admin generated classes, you actually only have to modify the generator configuration class. There’s either the getForm() method, or the getFormOptions() method that you can override to pass an extra option to the form constructor. This form is then used across all admin generated functionality. Very nice and central.

    Daniel

  11. […] So I was intrigued to read of the problems that the Context object can cause in your form classes, and how dependency injection gives us an easy way…. […]

  12. Alex on October 15th, 2009

    Hi Russ,

    Thanks. I had been reading about this on the webmozart’s website as well. With you concrete example, I now changed all the forms!

    I am wondering about dependency in the Symfony 2 mail function. Since it will be getting some settings with sfConfig(‘app_XXX’), which has to know about the context to get the right APP. I guess the same goes for the nahoMailplugin….

  13. Russ on October 15th, 2009

    @Alex: sfConfig::get() references the current loaded configuration, which is stored in a static array – so it is always available.

    The array is populated during the application configuration and project configuration stages, which are normally done for any symfony process, including tasks, so if I’m not mistaken it’s pretty safe to use. it is worth mentioning though that it will never fail – because it will always return the provided default if it doesn’t find the set value.

    Personally I’d try to inject any required settings into the forms’ options array also, but I haven’t come across an example yet so I’m keeping my options open for now!

  14. hal on October 19th, 2009

    Russ
    This article has helped me greatly. Thank you for writing it.
    Just one thing that I am curious about – why do you test the sfUser object using both “instance of” and is_object? Surely, “instance of” is enough?
    All the best!
    Hal

  15. Russ on October 20th, 2009

    @Hal: Yes it probably is ok to just use instanceof on its own here – I just get paranoid because a lot of php checking functions raise warnings and notices when they get the wrong types, or receive undeclared variables!

    I wonder why I wrote that… I need to test it again… Maybe there was a better reason!

  16. moh on December 8th, 2009

    What if the form has validation

    My code is something like this.
    user_role_id is required by default.

    if($user==’XYZ’)
    {
    $this->validatorSchema[‘user_role_id’]->setOption(‘required’, false);
    }

    It works for
    $this->form = new UserForm($user, array(“currentUser” => $this->getUser()));

    But its not working for
    $form->isValid()

  17. moh on December 9th, 2009

    My bad..
    Forgot to do this in update action

    $this->form = new UserForm($user, array(“currentUser” => $this->getUser()));

    Thanks!

  18. Russ on December 9th, 2009

    @moh: you should definitely think about throwing an exception in your configure() method if the currentUser option is not fulfilled!

  19. Maxime Gréau on February 18th, 2010

    @Daniel : when you override the getFormOptions() method, how do you to pass the user as an extra option to the form constructor because we don’t have access to the user object (in BaseXXXGeneratorConfiguration) ?

  20. Tonio on March 13th, 2010

    @Russ : Great explanation, still useful today. You have > breeding in your code snippets, you know… 🙂

  21. Russ on March 13th, 2010

    @Tonio – Argh not again… Blame WordPress ;o) I’ll fix… Thanks for the heads up.

  22. Rénald Casagraude on June 3rd, 2010

    @Maxime Gréau : you can inject sfUser in admin configuration, eg :


    //action.class.php
    public function preExecute()
    {
    $this->dispatcher->connect('admin.pre_execute', array($this, 'listenToAdminPreExecute'));

    parent::preExecute();
    }

    public function listenToAdminPreExecute(sfEvent $event)
    {
    // Inject sfUser in configuration
    $parameters = $event->getParameters();

    $parameters['configuration']->setUser($event->getSubject()->getUser());
    }

    Then override getFormOptions() :

    //*GeneratorConfiguration.class.php
    protected $user;

    public function setUser(sfUser $user)
    {
    $this->user = $user;
    }

    public function getUser()
    {
    return $this->user;
    }

    public function getFormOptions()
    {
    return array('user' => $this->getUser());
    }

    See also : http://prendreuncafe.com/blog/post/2010/02/17/User-Dependant-Forms-with-Symfony

  23. Marcelo on August 25th, 2010

    How can I pass a variable through edit action ?
    Because it’s already passing an object:
    $this->form = new SoForm($var);

  24. Marcelo on August 26th, 2010

    I got it, replaced the first array() by this $var

  25. Xavier on March 21st, 2011

    Hi,

    Thanks for this nice post. I’ll try to refactor my code to include this tip.

    xavier.

  26. FootPrints on July 25th, 2011

    […] # Passing values to FOrm classes from action. Symfony […]

  27. Tapper on April 4th, 2012

    Thanks a lot for your article – really helpful!
    Had big problems because I forgot to inject the parameter in the ‘edit’ AND ‘update’ action..
    :))

Leave a reply