Why sfContext::getInstance() Is Bad

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 service locator for all relevant core classes of the symfony framework: 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 separate 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
    }
  }
}

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
    }
  }
}

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;
      }
    }
    
  • 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 the previous code listing.

    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');
        }
      }
    }
    

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(...);

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

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

Discussion