Source Maven

Code Partition for Beau Simensen

Silex Service Providers and Controller Providers; What Is Safe To Do Where?

Since discussing writing Silex service providers a few edge cases have come up that I'd like to touch on. I've also learned more about controller providers and how they fit into the big picture. Here are a few more rules and guidelines to help make writing Silex service providers and controller providers a little easier.

Service Provider Registering Another Service Provider

One may be tempted to register another service provider from a service provider's register method. Take the following example:

use Silex\Application;
use Silex\Provider\DoctrineServiceProvider;
use Silex\ServiceProviderInterface;

public function MyServiceProvider implements ServiceProviderInterface
{
    public function boot(Application $app)
    {
    }

    public function register(Application $app)
    {
        // This is probably not wise.
        $app->register(new DoctrineServiceProvider);
    }
}

While there is nothing wrong with this from an interface perspective, it violates the spirit of the service provider contract.

One of the rules from Writing Silex Service Providers was to treat the Application type hint for register as if it were actually Pimple. Pimple does not have a register method so by this rule register should not be called from within register.

What about the fact that Pimple itself may some day support a register method of its own? Well, when that happens we can revisit the issue. :) Until then the existing rule holds.

But doesn't registering a service provider pretty much just defines more services? Yes, this is true. However, doing so hides the fact that the service provider is being registered from the user. This is not good.

Consider the example above where we are registering the Doctrine service provider. What happens if the user doesn't realize this and they register the Doctrine provider themselves? In some cases this may be mostly harmless but in others this could be very bad.

By registering another service provider directly we end up with two problems. First, we are hiding dependencies. Second, we are tying our service provider to a specific implementation.

Hidden Dependencies

Take a slightly extended example where Doctrine service provider is registered by a service provider:

use Silex\Application;
use Silex\Provider\DoctrineServiceProvider;
use Silex\ServiceProviderInterface;

public function MyServiceProvider implements ServiceProviderInterface
{
    public function boot(Application $app)
    {
    }

    public function register(Application $app)
    {
        // This is probably not wise.
        $app->register(new DoctrineServiceProvider);

        $app['myapp.someservice'] = $app->share(function () use ($app) {
            // do something with $app['db'];
        });
    }
}

Written this way our service provider has a hidden dependency on the Doctrine service provider.

Rather than register the Doctrine service provider directly we should document that the user needs to register it themselves. This allows them to register it and configure it however they see fit.

To quote Igor on why this is the best approach:

... it means you don't get to configure that provider when it is registered
it's about giving the user control and dependency inversion ...

Tied to a Specific Implementation

To take it a step further, we can document exactly what we need, namely that we need $app['db'] to be available and that it needs to be an instance of Doctrine DBAL Connection.

To help the user out we can say that this service can be provided by the Doctrine service provider and even use it in example code.

An example of this in action would be the requirements of the Doctrine ORM Service Provider:

Currently requires both dbs and dbs.event_manager services in order to work. These can be provided by a Doctrine Service Provider like the Silex or Cilex service providers. If you can or want to fake it, go for it. :)

As is usually the case, by not tying our service provider to another service provider directly we will also get the added benefit of increased testability for our service provider.

Responsibility of Controller Provider's connect() Method

The responsibility of a Controller Provider's connect method is to wire up controllers. That is it. The rule is simple. This means that services should not be defined in a controller provider.

A Single Class Can Provide Both Services and Controllers

When discussing controller services and splitting the service definitions from the routing, Igor reminded me of a simple truth about interfaces. Classes can implement more than one interface at a time.

This means that a class can implement both the service provider interface and the controller provider interface at the same time. This is a nice solution for building out a provider for controllers as services:

class MyAppControllersProvider implements
    ServiceProviderInterface,
    ControllerProviderInterface
{
    function boot(Application $app)
    {
    }

    function register(Application $app)
    {
        //
        // Define controller services
        //

        $app['myapp.hellocontroller'] = $app->share(function() use ($app) {
            return new MyApp\Controller\HelloController($app['some.service']);
        });
    }

    public function connect(Application $app)
    {
        $controllers = $app['controllers_factory'];

        //
        // Define routing referring to controller services
        //

        $controllers->get('/hello/{name}', 'myapp.hellocontroller:say')
            ->method('GET')
            ->bind('myapp.hello');

        return $controllers;
    }
}

This allows us to follow the rules of only defining services in register and only defining controllers in connect but keeping the code in the same class. Win!


One caveat to this is that we have to register and mount the provider separately. For example:

$myAppControllersProvider = new MyAppControllersProvider;

$app->register($myAppControllersProvider);
$app->mount('/', $myAppControllersProvider);

Mounting Controller Provider in boot()

It might seem logical to mount a controller provider in boot. While not strictly wrong since Application code is more or less safe to call from inside boot it is probably not the best choice.

Mounting a controller provider inside boot will take away some control of the controller provider from the user. For example, the user will lose the ability to mount the controller provider at a path of their choice.

So while one can do it, it would be best to do so sparingly. More often than not the right decision is probably to allow the user to mount the controller provider themselves.

Control

A common theme seen here is control. The question of mounting a controller in boot is similar to question of registering a service provider from within a service provider. Both are attempts to wrangle control from the user.

This control is important. We should do what we can to not limit it if we do not need to.

Join Us

Seriously, people, #silex-php rocks. Come join us if you want to discuss awesome things and learn Silex best practices. :)