Bundles, not extensible
A (Symfony) bundle usually contains just some service definition files as well as some configuration options which allow the user of that bundle to configure its behavior (e.g. provide server credentials, etc.). Bundle maintainers usually end up combining lots of these options in one bundle, because they all belong to the same general concept. For example, the NelmioSecurityBundle contains several features which are all related to security, but are code-wise completely unrelated. This means that this one package could have easily been split into several packages. Please note that I'm not making fun of this bundle, nor criticizing it, I'm just taking it as a nice example here. Besides, I've created many bundles like this myself.
The common reuse principle
Moving out functionality is great for the overall design of your packages. If you've heard about the package design principles, you may know about the Common reuse principle, which offers a guideline for splitting packages: if part of a package could be used without the rest of the package, then create a separate package for it.
For regular (library) packages, it's quite easy to follow the Common reuse principle. For bundles, it's much harder, because you can't easily split the container Extension
class and the Configuration
class. In the case of the NelmioSecurityBundle
, the Configuration class defines configuration options for all the separate parts of that bundle.
nelmio_security:
signed_cookie:
...
encrypted_cookie:
...
clickjacking:
...
external_redirects:
...
csp:
...
...
The NelmioSecurityExtension class wires all the service definitions and thus contains knowledge about all the separate security-related features of the bundle.
This poses several problems:
- If the bundle maintainer wants to implement some other security-related behavior, they'd have to modify both the
Extension
class and theConfiguration
class, but the rest of the package won't be touched. This means these classes themselves don't respect the open/closed principle: behavior can't be changed without actually modifying the code. - If someone else, who isn't part of the bundle maintainer's organization, wants to add security-related behavior to the bundle, they should either submit a pull request, or create their own bundle. The first option isn't a good one, because the maintainer may not agree about adopting that new behavior, or they don't accept pull requests at all. The second option isn't good either, since the bundle you just created, doesn't make sense on its own - it's basically an extension for the existing bundle. Finally, the configuration values would not be at the same location as the "main" bundle's, which is inconvenient for users.
Pondering these issues, I came to the conclusion that it might be really useful to have some kind of plugin or extension system for bundles. If the maintainer of the "main" bundle would allow users to register plugins, that bundle would basically become extensible. The plugin would be able to add configuration options in the same configuration tree as the main bundle. Then you can have:
- A separate definition for the configuration nodes of the plugin,
- A separate
load()
function to load and configure service definitions just for a particular plugin, - Separate packages for bundle plugins (optional).
Introducing the SymfonyBundlePlugins library
Now it's time to introduce the SymfonyBundlePlugins library which offers the functionality described above. If you want to install it in your project, run composer require matthiasnoback/symfony-bundle-plugins
.
The main bundle registers itself as a "bundle with plugins" by extending from the BundleWithPlugins
class (let's just take the NelmioSecurityBundle
again as an example):
use Matthias\BundlePlugins\BundleWithPlugins;
class NelmioSecurityBundle extends BundleWithPlugins
{
protected function getAlias()
{
return 'nelmio_security';
}
}
Now, each of the separate features of this bundle can be defined as a plugin by creating classes for them which implement BundlePlugin
:
use Matthias\BundlePlugins\BundlePlugin;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
class EncryptedCookiePlugin implements BundlePlugin
{
public function name()
{
return 'encrypted_cookie';
}
public function addConfiguration(ArrayNodeDefinition $pluginNode)
{
...
}
public function load(
array $pluginConfiguration,
ContainerBuilder $container
) {
...
}
}
In the addConfiguration()
you can add nodes to the configuration tree. For example:
public function addConfiguration(ArrayNodeDefinition $pluginNode)
{
$pluginNode
->arrayNode('names')
->prototype('scalar')->end()
->defaultValue(array('*'))
->end()
...
}
This would allow users to have the following Yaml configuration:
nelmio_security:
encrypted_cookie:
names: [a, b, c]
Inside the load()
method of the plugin you can do anything you'd usually do in your Extension
's load()
method, except, you don't need to process the configuration anymore: it's already provided. So given the above configuration, $pluginConfiguration
would contain something like this: ['names' => ['a', 'b', 'c']]
.
Enabling bundle plugins
When a bundle extends BundleWithPlugins
, it can't have its own Extension
or Configuration
class anymore, so you might want to define general service definitions etc. in something like a CorePlugin
, which is just another bundle plugin itself.
To make sure this plugin is always enabled, override the alwaysRegisteredPlugins()
method of your bundle:
class NelmioSecurityBundle extends BundleWithPlugins
{
...
protected function alwaysRegisteredPlugins()
{
return [new CorePlugin()];
}
}
All other plugins can be enabled by providing instances of them to the constructor of the bundle when you instantiate it in your application kernel:
class AppKernel extends Kernel
{
public function registerBundles()
{
return array(
...,
new NelmioSecurityBundle([
new EncryptedCookiePlugin(),
...
])
);
}
}
When to use bundle plugins
- When your bundle offers separate features, which you'd still like to offer as part of one bigger concept.
- When these separate features would never be used without the "main" bundle.
- When you want to move the separate features to separate packages.
When not to use this
- When the separate features could as well be used by people who don't want to use the "main" bundle.
- When you don't want others to add new features to your bundle.
I hope you like it. If you have questions or suggestions, please let me know on the issues page of the project.
Also, thank @dennisdegreef for reviving (the test suite of) this project!
Great, I was actually looking for something like this :)
I ended-up creating multiple bundles (just for integration).
https://github.com/rollerwo... (core bundle)
https://github.com/rollerwo... (additional bundle, config and services only)
https://github.com/rollerwo... (additional bundle, config and services only)
And its only going to get worse with the addition of other storage engines!
Nice one, thanks !
Thank you too - I hope it will be of use after all :)
But why not just create another Bundle? Aren't your Plugins just a "lighter" Bundle contract? With this you basically could then go back and just have the FrameworkBundle support Plugins, no more need for Bundle's, back to Plugins! Sure its nice to have even less code and a single class to integrate into Symfony but it seems more like what a novice would look for until they understood how to write a Bundle.
You're totally right, Lukas :) These plugins are just light-weight bundles, I like how you call it a "lighter contract".
The major advantage to a plugin for me is that it makes the relation between the main bundle and sub-bundles more explicit. Also, configuration can still be in one place, which is nice in certain cases. And you won't have these very big Extension/Configuration classes anymore, which is why I initially created this library.
Thanks. Matthias. As always, great library.
Thank you!
Great library and saved me a couple of hours last friday ;)
Thnx!
Thanks for letting me know!
Speaking of plugins, do you think that it is possible to enable bundle (vel. plugin) ie. registering it in kernel through web interface?
I'm not sure. The Zikula CMS people have been trying to do this, don't know how far they came with it. It is a bit dangerous, since loading an extra bundle might crash the application and you can't easily recover from that.
I had this same idea a while back, got to see you have implemented it!
My use cases were:
Forms - lots of plugins offering new field types, without needing a whole bundle
CMS - plugins for custom field types (similar to forms)
Does anyone else thing this would be great for CMS/forms?
Thanks Matthias for writing an implementation, I really hope some people start using it!
Be interesting to hear other peoples ideas on the types of bundles they think this would benefit.
Oh yep… it's ideal for forms types, I thought the same