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.

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)){thrownew 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 classpublicfunction configure(){if(!($this->getOption("currentUser") instanceof sfUser)){thrownew 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

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.

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 ?

@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!

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.

@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 🙂

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.

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….

@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!

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

@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!

@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) ?

Tonio on
March 13th, 2010

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