The problem
With a new bundle release you may want to rename services or parameters, make a service private, change some constructor arguments, change the structure of the bundle configuration, etc. Some of these changes may acually be backwards incompatible changes for the users of that bundle. Luckily, the Symfony DependenyInjection component and Config component both provide you with some options to prevent such backwards compatibility (BC) breaks. If you want to know more about backwards compatibility and bundle versioning, please read my previous article on this subject.
This article gives you an overview of the things you can do to prevent BC breaks between releases of your bundles.
Renaming things
The bundle itself
You can't, without introducing a BC break.
// don't change the name
class MyOldBundle extends Bundle
{
}
The container extension alias
You can't, without introducing a BC break.
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
class MyBundleExtension extends Extension
{
...
public function getAlias()
{
// don't do this
return 'new_name';
}
}
Parameters
If you want to change the name of a parameter, add another parameter with the desired name, which receives the value of the existing parameter by substitution:
parameters:
old_parameter: same_value
new_parameter: %old_parameter%
class MyBundleExtension extends Extension
{
public function load(array $config, ContainerBuilder $container)
{
$container->setParameter('old_parameter', 'same_value');
$container->setParameter('new_parameter', '%old_parameter%');
}
}
Now existing bundles or the user may still change the value of old_parameter
and that change will propagate to the new parameter too (thanks WouterJNL for suggesting this approach!).
Config keys
The container extension alias can not be changed without causing a BC break, but it is possible to rename config keys. Just make sure you fix the structure of any existing user configuration values before processing the configuration:
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('my');
$rootNode
->beforeNormalization()
->always(function(array $v) {
if (isset($v['old_name'])) {
// move existing values to the right key
$v['new_name'] = $v['old_name'];
// remove invalid key
unset($v['old_name']);
}
return $v;
})
->end()
->children()
->scalarNode('new_name')->end()
->end()
;
}
}
Service definitions
If you want to rename a service definition, add an alias with the name of the old service:
services:
new_service_name:
...
old_service_name:
alias: new_service_name
This may cause a problem in user code, because during the container build phase they may call $container->getDefinition('old_service_name')
which will cause an error if old_service_name
is not an actual service definition, but an alias. This is why user code should always use $container->findDefintion($id)
, which resolves aliases to their actual service definitions.
Method names for setter injection
services:
subject_service:
calls:
# we want to change the name of the method: addListener
- [addListener, [@listener1]]
- [addListener, [@listener2]]
This one is actually in the grey area between the bundle and the library. You can only change the method names used for setter injection if users aren't supposed to call those methods themselves. This should actually never be the case. You should just offer an extension point that takes away the need for users to call those methods. The best option is to create a compiler pass for that:
namespace MyBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class InjectListenersPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$subjectDefinition = $container->findDefintion('subject');
foreach ($container->findTaggedServiceIds('listener') as $serviceId => $tags) {
$subjectDefinition->addMethodCall(
'addListener',
array(new Reference($serviceId))
);
}
}
}
You need to add this compiler pass to the list of container compiler passes in the bundle class:
class MyBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new InjectListenersPass());
}
}
From now on, users can register their own listener services like this:
services:
user_listener_service:
tags:
- { name: listener }
The compiler pass will then call the addListener()
method for each of the registered listeners.
From now on you can change the method name used for this type of setter injection at any time, because the user doesn't call it explicitly anymore.
Changing visibility
Parameters
This doesn't apply to parameters, they are all public.
Service definitions
Service definitions can be public or private:
services:
a_public_service:
# a service is public by default
public: true
a_private_service:
public: false
When a definition is marked "private" you can't fetch it directly from the container by calling $container->get($id)
. Private services can only be used as arguments of other services.
If a service definition was previously public, you can't change it to be private, because some users may rely on it to be available by calling $container->get($id)
. The only option here is to rename the service, make it private, and then add an alias for it with the old name. This will effectively make the service public again (as Christophe Coevoet mentioned in a comment: it is also possible to create private aliases).
services:
a_public_service:
# an alias is always public
alias: a_private_service
a_private_service:
public: false
If a service definition was previously private, you can change it to public at any time. There are no ways in which a user could have used a private service that won't work when the service becomes public.
Changing values
Service definitions
I think we should agree on arguments of service definitions to be private property of the bundle. Users should not rely on them to stay the same between two releases.
Config values
If you want to change allowed values for bundle configuration, you should fix existing config values, by using the beforeNormalization()
method (we discussed this previously).
If you want to remove existing config keys, you will have to unset them using beforeNormalization()
too, or they will trigger errors about unknown keys.
If you want to add new required configuration values, you should provide sensible defaults for them, to accomodate existing users:
$rootNode
->children()
->scalarNode('required_key')
->isRequired()
->defaultValue('sensible_default')
end()
->end()
;
If the parent key is a new key and is allowed to be missing, you should add ->addDefaultsIfNotSet()
to the parent array node:
$rootNode
->children()
->arrayNode('optional_key')
->addDefaultsIfNotSet()
->children()
->scalarNode('required_key')
->isRequired()
->defaultValue('sensible_default')
end()
->end()
->end()
->end()
;
New optional config keys should be no problem.
Conclusion
Of course, if the change you want to introduce is not compatible with one of the proposed BC-friendly options, you should release a new major version of your bundle. No problem with that, it just needs to be a conscious decision and you might want to try a bit harder before you decide to do that.
I think this article contains a fairly exhaustive list of ways to prevent BC breaks between bundle releases. If you have a suggestion for this list, just let me know.
Just a small issue in your article: aliases are not always public. It is possible to create private aliases as well
Ah, I didn't know that. Thanks! I got it mixed up with "using an alias you can effectively make a private service public again". It makes sense though and seems very useful to me.
You can make public aliases of private services (the service id will not be usable at runtime, but the alias id will), public aliases of public services (both ids will be available at runtime) or private aliases of any service (the alias id will never be available at runtime as it will always be deleted from the container after resolving the alias references to the real service)
By the way, I added this to the article.
Thanks! Great collection of tips to avoid BC breaks! This should be in every developer's tool belt, IMO.
Just a typo, it should be 'When a definition is marked "private"' instead of "public" in 'Changing visibility/Service definitions'.
Thanks!
I fixed the typo.
In "Renaming things: Parameters", I would use:
parameters: { new_parameter: "%old_parameter%" }
instead. This allows other bundles which are registered after the bundle with those parameters to change the parameter value of old_parameter (and it means new_parameter will have that change too)
Excellent suggestion! I will change that.