Introducing the SymfonyBundlePlugins package

Written by Matthias Noback - - Aggregated on Monday July 6, 2015

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:

  1. If the bundle maintainer wants to implement some other security-related behavior, they'd have to modify both the Extension class and the Configuration 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.
  2. 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:

  1. A separate definition for the configuration nodes of the plugin,
  2. A separate load() function to load and configure service definitions just for a particular plugin,
  3. 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

  1. When your bundle offers separate features, which you'd still like to offer as part of one bigger concept.
  2. When these separate features would never be used without the "main" bundle.
  3. When you want to move the separate features to separate packages.

When not to use this

  1. When the separate features could as well be used by people who don't want to use the "main" bundle.
  2. 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!


« Experimenting with Broadway - Matthias Noback

Matthias Noback - Compartmentalization in the PHP … »