Symfony2: Some things I don't like about Bundles
Matthias Noback
This article could have been titled “Ten things I hate about bundles”, but that would be too harsh. The things I am going to describe, are not things I hate, but things I don’t like. Besides, when I count them, I don’t come to ten things…
Bundle extension discovery
A Symfony2 bundle can have an Extension
class, which allows you to load service definitions from configuration files or define services for the application in a more dynamic way. From the Symfony documentation:
[The Extension] class should live in the DependencyInjection
directory of your bundle and its name should be constructed by replacing the Bundle
suffix of the Bundle
class name with Extension
. For example, the Extension
class of AcmeHelloBundle
would be called AcmeHelloExtension
[…]
When you conform to the naming convention, your bundle’s Extension
class will automatically be recognized. But how? A quick look in the Kernel
class reveals to us that it calls the method getContainerExtension()
on each registered bundle (which is an object extending implementing BundleInterface
):
namespace Symfony\Component\HttpKernel;
...
abstract class Kernel implements KernelInterface, TerminableInterface
{
protected function prepareContainer(ContainerBuilder $container)
{
...
foreach ($this->bundles as $bundle) {
if ($extension = $bundle->getContainerExtension()) {
$container->registerExtension($extension);
...
}
...
}
...
}
}
If the getContainerExtension()
method of a bundle returns anything, it is assumed to be an instance of ExtensionInterface
, in other words a service container extension.
My bundles almost always look like this:
namespace Matthias\Bundle\DemoBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class MatthiasDemoBundle extends Bundle
{
}
There is no implementation for getContainerExtension()
there, so it must be in the parent class. And it is:
namespace Symfony\Component\HttpKernel\Bundle;
...
abstract class Bundle extends ContainerAware implements BundleInterface
{
...
public function getContainerExtension()
{
if (null === $this->extension) {
$basename = preg_replace('/Bundle$/', '', $this->getName());
$class = $this->getNamespace().'\\DependencyInjection\\'.$basename.'Extension';
if (class_exists($class)) {
$extension = new $class();
// check naming convention
$expectedAlias = Container::underscore($basename);
if ($expectedAlias != $extension->getAlias()) {
throw new \LogicException(sprintf(
'The extension alias for the default extension of a '.
'bundle must be the underscored version of the '.
'bundle name ("%s" instead of "%s")',
$expectedAlias, $extension->getAlias()
));
}
$this->extension = $extension;
} else {
$this->extension = false;
}
}
if ($this->extension) {
return $this->extension;
}
}
}
My goodness, this is some ugly code, but more imporantly: this is magic! My extension class is nowhere explicitly used, nor registered as the one and only service container extension for my bundle. It is simply infered from the existence of a file with the expected name, containing the expected class (there is not even a check for the right interface), that I have a service container extension for my bundle! I don’t like that at all.
Naming conventions
Even worse: the naming conventions prevent you from easily moving your Bundle
and Extension
class to another namespace (like you probably have noticed yourself).
I can move Matthias\Bundle\DemoBundle\MatthiasDemoBundle
to Matthias\Bundle\TestBundle\MatthiasTestBundle
with a simple search-and-replace, but I can not just move its DependencyInjection\MatthiasDemoExtension
class to Matthias\Bundle\TestBundle\DependencyInjection
, The class itself has to be renamed to MatthiasTestExtension
, or it won’t be recognized anymore.
Also somewhat annoying: the Bundle::getContainerExtension()
puts a constraint on the alias of the service container extension: when my bundle is called MatthiasTestBundle
, its alias should be matthias_test
. But there is no real need for this, it is just a policy to prevent developers from overriding each other’s (or worse: the framework’s) bundle configuration.
This last rule is enforced in quite a strange way. Remember where the exception is being thrown? Yes, inside my own bundle class! I can easily override getContainerExtension()
and skip the validation of the alias of my service container extension…
Registering service container extensions yourself
Because of the bundle magic described above, I like to implement the getContainerExtension()
method myself and return an instance of the extension. The name of this class can be anything I like.
namespace Matthias\Bundle\TestBundle;
use Matthias\Bundle\TestBundle\DependencyInjection\CanBeAnything;
class MatthiasTestBundle extends Bundle
{
public function getContainerExtension()
{
return new CanBeAnything();
}
}
Now creation logic is also entirely on my side, where I like it to be.
Naming conventions, part two
As mentioned above, an extension has an alias, which you can retrieve by calling its method getAlias()
. The standard implementation of this method is:
namespace Symfony\Component\DependencyInjection\Extension;
...
abstract class Extension implements ExtensionInterface, ConfigurationExtensionInterface
{
...
public function getAlias()
{
$className = get_class($this);
if (substr($className, -9) != 'Extension') {
throw new BadMethodCallException('This extension does not follow the naming convention; you must overwrite the getAlias() method.');
}
$classBaseName = substr(strrchr($className, '\\'), 1, -9);
return Container::underscore($classBaseName);
}
...
}
This function also checks if we have followed the naming convention for extension classes. Since we have chosen to skip the naming convention check in the bundle class, we might just as well skip the check in the extension class too, and just implement the getAlias()
method ourselves:
namespace Matthias\Bundle\TestBundle\DependencyInjection;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
class CanBeAnything extends Extension
{
public function getAlias()
{
return 'matthias_test';
}
}
Perfectly fine! Now moving a Bundle
or Extension
class won’t break any existing configuration under the key matthias_test
in config.yml
and the likes.
Duplicate knowledge: the extension alias
As you may know, when you create a Configuration
class for your bundle, you have to provide the configuration key, also known as the service container alias, as the name of the root node of the TreeBuilder
instance:
namespace Matthias\Bundle\TestBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
// there it is: the service container alias!
$rootNode = $treeBuilder->root('matthias_test');
...
return $treeBuilder;
}
}
It has always bothered me that this is somehow duplicate knowledge: it should not be necessary to use the exact same string here, which can also be retrieved by calling getAlias()
on the Extension
class. But the Configuration
class can not do this: it has no access to the service container extension. Instead, it has to be the other way around: the Extension
needs to provide its alias to the Configuration
:
namespace Matthias\Bundle\TestBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class CanBeAnything extends Extension
{
public function load(array $config, ContainerBuilder $container)
{
$configuration = new Configuration($this->getAlias());
$processedConfig = $this
->processConfiguration($configuration, $config);
}
public function getAlias()
{
return 'matthias_test';
}
}
Then the Configuration
class needs to look something like this:
namespace Matthias\Bundle\TestBundle\DependencyInjection;
class Configuration implements ConfigurationInterface
{
private $alias;
public function __construct($alias)
{
$this->alias = $alias;
}
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
// no more duplication!
$rootNode = $treeBuilder->root($this->alias);
...
return $treeBuilder;
}
}
In conclusion
Well, it takes some extra steps, but only a few, and very easy ones. They will make it much easier to move bundles to another namespace without many other things breaking. They will also free you from naming conventions that would have otherwise been enforced.
Maybe most importantly: they finally remove some magic from bundles, that has been there as some kind of a reminiscence of the old days of symfony 1. It is not very modern to automatically load a file that is located in a certain directory and has a certain name.
(The same goes for console commands, for also registered automagically. See one of my previous post for more about this.)
One final remark: I still recommend you to conform to these conventions:
- Your service container alias/configuration root should correspond to the name of your bundle, to prevent naming collissions.
- Your
Extension
andConfiguration
classes should still be in theDependencyInjection
directory/namespace.
These are good conventions, and they make it easy for any developer to understand and work with your bundle.