New Symfony Way: Scratch Bundle for Packages with Compiler Passes

In the same way silex have service providers you can also have the same in Symfony, no need for bundles to define services. You can just plug some compiler passes and special configuration classes but really there is no need to create a bundle to have configuration capabilities.

The reasons for not having any bundle is that there is really no need for a bundle when you are creating an application package. Creating a bundle pulls your direction away from creating a reusable package outside symfony whereas if you create a standalone package and offer some service definitions it ends up being a better choice.

The addon which i have not mentioned is that there are really good projects out there like PageKit, even yucky projects like laravel that use symfony components or even others that do use the config and di components and yet do not use the bundle concept. For them the world of bundles is far away and will never be reached. Whereas if you design a library/package standalone it can have providers for these too. The end result is more reusability.

Let’s start with a good example. At the end of this blog from Matthias Noback’s blog post we have learned how to register ancillary event dispatcher services that are decoupled from the symfony default event_dispatcher system and how we register listeners or subscribers with that sub event system.

Here is the same code showing how you register the compiler pass in the way proposed in that blog post:

use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\HttpKernel\DependencyInjection\RegisterListenersPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
 
class YourBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        $container->addCompilerPass(
            new RegisterListenersPass(
                'domain_event_dispatcher',
                'domain_event_listener',
                'domain_event_subscriber'
            ),
            PassConfig::TYPE_BEFORE_REMOVING
        );
    }
}

and add the service definitions:

services:
    domain_event_dispatcher:
        class: Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher
        arguments:
            - @service_container

And how you create a listener (and extra bonus for the subscriber):

services:
    some_domain_event_listener
        class: ...
        tags:
            -
                name: domain_event_listener
                event: my_domain_event
                method: onMyDomainEvent
    some_domain_event_subscriber
        class: ...
        tags:
            -
                name: domain_event_subscriber

There is I believe another way to do this. What is truly required is a way to plug directly into the kernel (like it is done in bundles), the compiler passes that create this service definitions and then be done with it.

Let’s take a stab. First we can modify generically our AppKernel.php like this:

    protected function prepareContainer(ContainerBuilder $container)
    {
        $extensions = array();
        foreach ($this->bundles as $bundle) {
            if ($extension = $bundle->getContainerExtension()) {
                $container->registerExtension($extension);
                $extensions[] = $extension->getAlias();
            }
 
            if ($this->debug) {
                $container->addObjectResource($bundle);
            }
        }
        foreach ($this->bundles as $bundle) {
            $bundle->build($container);
        }
 
        $this->hookCompilers($container);
 
        // ensure these extensions are implicitly loaded
        $container->getCompilerPassConfig()->setMergePass(new MergeExtensionConfigurationPass($extensions));
    }

Notice the hookCompilers is a custom method added:

    private function hookCompilers(ContainerBuilder $container)
    {
        foreach ($this->registerCompilers() as $compilerRow) {
            $instance = $compilerRow[0];
            $type = $compilerRow[1];
            $debug = $compilerRow[2];
            if (!$debug || $container->getParameter('kernel.debug')) {
                $container->addCompilerPass($instance, $type);
            }
        }
    }

But that will not need to change. The registerCompilers method is equivalent to the registerBundles method that is well known by the bundle system users. This registerCompilers method will look like:

    public function registerCompilers()
    {
        $configuration = new Configuration();
        $configuration->setEvaluateExpressions(true);
 
        return [
            [new ValidateServiceDefinitionsPass($configuration), PassConfig::TYPE_AFTER_REMOVING, true],
            [new FixValidatorDefinitionPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, true],
            [new RegisterListenersPass(Dispatchers::READ, Dispatchers::READ_LISTENER, Dispatchers::READ_SUBSCRIBER), PassConfig::TYPE_BEFORE_REMOVING, false],
            [new RegisterListenersPass(Dispatchers::WRITE, Dispatchers::WRITE_LISTENER, Dispatchers::WRITE_SUBSCRIBER), PassConfig::TYPE_BEFORE_REMOVING, false],
        ];
    }

Notice that we have added several compiler passes here. The first two are coming from the package for validating the service definitions and stuff that we saw in a previous post. Now though they are totally standardized. The 2 last compiler passes added are two new event sub systems, easily pluggable just by adding new rows here. After this tapping into a different event dispatcher is totally straightforward.

Mind that we have just compiled the constants into a class Dispatchers.php:

<?php
 
namespace Grace;
 
class Dispatchers
{
    const READ = 'read_event_dispatcher';
    const READ_LISTENER = 'read_event_dispatcher_listener';
    const READ_SUBSCRIBER = 'read_event_dispatcher_subscriber';
 
    const WRITE = 'write_event_dispatcher';
    const WRITE_LISTENER = 'write_event_dispatcher_listener';
    const WRITE_SUBSCRIBER = 'write_event_dispatcher_subscriber';
}

You can now just define a subscriber like:

    grace.some_domain_event_subscriber:
        class: Grace\ExampleSubscriber
        tags:
            -
                name: Grace\Dispatchers::READ_SUBSCRIBER

The whole story can be better seen in a diff of this PR in matthew-7-12 project https://github.com/cordoval/matthew-7-12/pull/70/files.

A note on the logging capability of these new event dispatcher sub systems was made by @sstok. The concern is that on debug mode the event dispatcher is actually wrapped like this:

        <service id="debug.event_dispatcher" class="%debug.event_dispatcher.class%">
            <tag name="monolog.logger" channel="event" />
            <argument type="service" id="debug.event_dispatcher.parent" />
            <argument type="service" id="debug.stopwatch" />
            <argument type="service" id="logger" on-invalid="null" />
        </service>

Perhaps we can do a conditional and check for the interface they extend or class of the event dispatcher base that they extend and decorate accordingly our dispatchers too in case we are under the development mode. This of course is not hard to do and it could be accomplished by a DecorateRegisterListenerPasses.php compiler pass too.

An also good example on how to split a bundle and a dependency injection provider package is https://github.com/rollerworks/RollerworksSearchBundle. This bundle actually requires its dependency extension component in its composer.json:

    "require": {
        "php": ">=5.3.3",
        "rollerworks/search-symfony-di": "1.*@dev",
        "symfony/framework-bundle": "~2.1",
        "symfony/finder": "~2.1"
    },

Offering very good separation for those who integrate Rollerworks Search package without having to be tied to use a bundle. This is a better approach than the bundle one. You can take a look at the package here https://github.com/rollerworks/rollerworks-search-symfony-di/tree/master/src. In there you can also see a custom loader which is nice for when you go all the way with a bundle-less approach.

This approach will do great good to those systems that only use some symfony components and yet leverage their own component subsystem. Also for those projects that require flexibility, especially when dealing with DDD this is a desirable architecture to pursue.

Hope you like the approach, comment and don’t forget to retweet sil vous plait!

One thought on “New Symfony Way: Scratch Bundle for Packages with Compiler Passes

  1. First of all I want to say wonderful blog!
    I had a quick question in which I’d like to ask if you do not mind.
    I was interested to find out how you center yourself
    and clear your thoughts prior to writing. I’ve had a hard time clearing my mind in getting my thoughts out.
    I truly do enjoy writing however it just seems like the
    first 10 to 15 minutes are generally lost just trying to figure
    out how to begin. Any recommendations or tips? Appreciate it!

Leave a Reply

Your email address will not be published. Required fields are marked *