Why sfContext::getInstance() Is Bad
- Posted on
- Categories: Best Practices, Symfony
- Comments
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), thesave()
method fails.Problem #2: Testability
For testing the
Product
class, you have to create the context including all of its dependencies. That means parsingfactories.yml
, reading the configuration files, instantiatingsfController
,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, yourProduct
test will suddenly fail, even if the classProduct
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