Navigation

The “default” context does not exist.

Is there any symfony developer out there who never stumbled upon this dreadful error message? I doubt it. Recently, a lot of posts have been made on the user mailing list asking for explanations and fixes. A quick search on Google for the exact phrase even returns 480 results!

The reason for this error is quite simple though: It’s because you used sfContext::getInstance(). And you should never do that.

MVC and the Context

The class sfContext is the glue of the controller layer in symfony. It keeps all the relevant controller classes together: sfUser, sfResponse, sfRequest, sfController as well as some others. The reason for its existence is simplicity. These classes now only need a reference to the context to access each other. The class sfController, for instance, contains a protected member variable $context, by which it can access all the other objects without having to store seperate references.

sfContext implements the Singleton design pattern. Thus you can retrieve one of the few context instances by calling sfContext::getInstance() with an optional first parameter: The name of the context. If the first parameter is not given, the name is assumed to be “default”. But sfContext is no real Singleton. No new instance will ever be created when you call sfContext::getInstance(). Instead, you need to call sfContext::createInstance() first. If you miss to do that, you will receive exactly the error at the introduction of my post.

Why to Avoid Singletons

The Singleton pattern is actually one of the worst design patterns by the Gang-of-Four[1]. Because you hard-code the name of the singleton class, you create a dependency that is hard and partially impossible to substitute from outside. As a result, your class becomes unflexible and very hard to unit-test. Let’s look at a common mistake:

class Product
{
  public function save()
  {
    if (sfContext::getInstance()->getUser()->hasAttribute('foobar'))
    {
      // do something
    }
  }
}

Listing 1

This implementation bears the following problems:

Problem #1: Flexibility

If you have multiple sfUser instances, you are not able to tell the product on which instance it should depend. The product always fetches the instance registered in the context. Also, if the context is not available (for instance in console tasks), the save() method fails.

Problem #2: Testability

For testing the Product class, you have to create the context including all of its dependencies. That means parsing factories.yml, reading the configuration files, instantiating sfController, sfRequest and many more classes. Doing so adds substantial overhead to your tests and makes them slow and prone to errors. If any of the operations during the context instantiation fails, your Product test will suddenly fail, even if the class Product is perfectly fine!

How to Avoid Singletons

Instead of hard-coding the call to sfContext::getInstance(), you should pass the context object directly to the object that needs it. If you don’t even need the context, but only one of its references like the user, pass that object instead.

class Product
{
  protected $user = null;
 
  protected function setUser(sfUser $user)
  {
    $this->user = $user;
  }
 
  public function save()
  {
    if ($this->user instanceof sfUser && $this->user->hasAttribute('foobar'))
    {
      // do something
    }
  }
}

Listing 2

This technique is called Dependency Injection. If you are not familiar with Dependency Injection, I recommend you to read Fabien Potencier’s introduction to Dependency Injection. Basically there are two ways two inject dependencies into an object:

Constructor Injection

This type of Dependency Injection is used when a dependency is required. Because the dependency needs to be passed in the constructor, no object can ever be created when this dependency is not fulfilled.

class Product
{
  protected $user = null;
 
  protected function __construct(sfUser $user)
  {
    $this->user = $user;
  }
}

Listing 3

Setter Injection

This type of Dependency Injection is used when a dependency is optional. Because you can never be sure whether the setter has been called, you always have to verify whether the object exists before accessing its operations and properties, as we did in Listing 2.

Alternatively, you can also use setter injection for required dependencies if you can not override the constructor without breaking the class (this is the case for Doctrine records). In that case, you simply throw an exception if the dependency is not available.

class Product
{
  public function save()
  {
    if (!$this->user instanceof sfUser)
    {
      throw new LogicException('The user must be set before saving');
    }
  }
}

Listing 4

What You Gained

Because you can now inject any user object into your class, you can test the Product class very easily:

class StubUser extends sfUser
{
  public function hasAttribute($name)
  {
    return $name == 'foobar';
  }
}
 
$t->comment('Products with the attribute "foobar" do something special upon saving');
 
  $p = new Product();
  $p->setUser(new StubUser());
  $p->save();
  $t->is(...);

Listing 5

Exceptions

In very few cases, you cannot avoid accessing the context by using sfContext::getInstance(). This is almost always the case when symfony manages the construction of an object. If you configure symfony to use a custom object by modifying factories.yml, you still won’t be able to inject custom objects.

This will change as soon as symfony uses the new Dependency Injection container for the construction of the core classes.

An example for this problem is extending sfResponse. You can tell symfony to use a custom response, but you cannot tell symfony to inject the context or other objects into it (except for if you write a custom sfFactoryConfigHandler). In this case, and because sfResponse is part of the controller layer, you should be pragmatic and just use sfContext::getInstance().

Conclusion

Always try to avoid calling sfContext::getInstance(). Many of symfony’s classes in the controller layer already store a reference to the context, so you can use that one instead. If no reference is available, inject the reference to the context or to whichever class you need.

References

[1] E. Gamma, R. Helm, R. Johnson, J. Vlissides: Design Patterns. Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995

Posted Wednesday, July 1st, 2009 at 08:24
Written by: | Filed Under Category: Best Practices
You can leave a response, or trackback from your own site.

22

Responses to “Why sfContext::getInstance() Is Bad”

John

Great explanation,

thanks,
John

Thanks for drawing attention to this topic. I’m also really looking forward to the new DI container in symfony. I’m even thinking about using it myself since it was recently introduced as a component, but I’ll have to wait until our dev server run PHP 5.3.

Matthias

I never got this error message. In what situation do you get it?

Bernhard

@Matthias: Just try accessing sfContext::getInstance() in a task. The problem appears when sfContext::createInstance() has not been called and thus no context is loaded.

Dejan Spasic

I you for the great explanation. I think a headline like “Why sfContext::getInstance() smells” will be a better choice :)

Dejan Spasic

i mean thank you and not I you :/

Peter Bowyer

Great article but one typo in listing 5:

$p->setUser(new StubUser();
There’s a missing closing brace!

Bernhard

@Peter: Thank you, fixed :-)

Perfect explanation. Thx!

There’s one more thing to add here: if you REALLY need a context in an sfTask (symfony command line task), you can use the following:

protected function execute($arguments = array(), $options = array())
{
$databaseManager = new sfDatabaseManager($this->configuration);
sfContext::createInstance($this->configuration);

// your code goes here
}

gpilotino

I can see another situation where you cannot avoid using sfContext::getInstance(): the admin generator.
(ie. how can you have a table_method / peer_method filtered on user credentials without accessing context from the Model ?)

David Clark

THANKS ;
I have been struggling with dependency injection forever, this really helps!

jwesker

@gpilotino, Yeah, how is sfContext::getInstace() avoidable from the generator, and also from the form classes? is it possible to inject dependency on the forms too?

I’ve usually come across this when using sfForm and I get around this by using the constructor injector mentioned above but pass the object I want as an option i.e.

$form = new myForm(array(), array(‘sf_guard_user’ => $user));

Passing it in this manner allows me to access it in the form class via $this->options[‘sf_guard_user’];

Interested

And how about I18n or other helpers loading in Form, or Model class (sfContext::getInstance()->getConfiguration()->loadHelpers(‘I18N’);)

John

How is it possible to get “multiple sfUser instances” ?

Bernhard

@Interested: Just inject the I18N instance into the form. I’m doing that too.

$form = new myForm($i18n, …);

And concerning the helpers… it’s probably better not to use them at all and rely on the underlying implementation. format_date(), f.i., is just a wrapper for sfNumberFormat. So use sfNumberFormat instead.

@John: What do you mean? In the form? Inject them.

[…] en contre-exemple, car j’ai pu souvent la trouver sur certains blogs ou forums, à tort (ici un très bon article, en anglais, qui explique […]

Load the Url symfony helper in a model or form class…

I’m doing some symfony work and needed to use the Url helper in a form class to return an error message that included a link. I would consider this one of those exceptions where you really need to be able to have markup in a class rather than in a tem…

[…] If you need to relay your Doctrine events to symfony, you might get tempted to create a dependency to sfContext in your models. As we all know (right?) this is a path straight down to hell. […]

[…] problematyczną zależnością. Dobre argumenty popierające tą tezę przedstawiono w artykule "Why sfContext::getInstance() Is Bad" i prezentacji "30 Symfony Best Practices (slajd 58)".Tweet6 października 2009 (Jakub Zalas) […]

Thomas

I don’t understand why you pin this negative label on the sfContext::getInstance() and the singleton design pattern. Like any tool in a developer toolkit, if badly used, a design pattern can be more problematic than helpful.

In your examples, the problem is not the use of the singleton design pattern but rather the junction of a model class (an object that is independent from the MVC application and that typically would represent an external resource, from a database or similar) with an object that is intrinsically tied to the Symfony MVC application pattern, which itself is entirely tied to, as you explained, the request, controller and view ensemble.

Regardless, the singleton design pattern is a useful development pattern which has its advantages (ensure uniqueness of an instance, provide single-point access to a resource/model) and its drawbacks (such as preventing garbage collection). Be smart, use it when it applies.

Leave a Reply

 

Additional Resources