Source Maven

Code Partition for Beau Simensen

Writing Silex Service Providers

So you've gotten to the point in your Silex application that you want to start breaking it out into modular pieces. Silex service providers to the rescue! The service provider interface appears to be really simple but do you know which things can safely be done in each method?

First, a high level overly generalized anatomy lesson of a Silex service provider:

namespace Acme\AwesomePackage;

use Silex\Application;
use Silex\ServiceProviderInterface;

class WhizbangServiceProvider implements ServiceProviderInterface
{
    public function register(Application $app)
    {
        // Define services here.
    }

    public function boot(Application $app)
    {
        // Configure the application and *carefully* use services.
    }
}

The primary purpose of a service provider is to configure the service container.

The what now?

The Silex Application class extends Pimple. This means that Application is a service container, more formally known as a Dependency Injection Container. This can be extremely useful once you know and understand about Dependency Injection Containers. For new users this can instead be very confusing.

If you are new to service containers or Dependency Injection, it would be a good idea to read up on the concept. If you are new to Pimple, reading up on it is going to be extremely important. Pimple's documentation is pretty sparse but dense.

One important aspect of a service container, and specifically in the case of Silex, is laziness.

Laziness

Services should be lazy. This means that where possible a service should not be instantiated until it is needed. With respect to a Silex application, if a service is accessed before run(), chances are you are doing it wrong!

Pimple does a pretty good job of this if you understand what this means and use it properly.

Here is a basic example of laziness in action:

<?php

$app['whirligig'] = $app->share(function() {
    return new Whirligig;
});

$app['whizbang.thingy'] = $app->share(function($app) {
    return new Whizbang\Thingy($app['whirligig']);
});

In this case, two services are defined. The whirligig service is referenced, but it is referenced from inside the whizbang.thingy service definition. This makes both of these services lazy.

A general rule, when dealing with a typical Silex/Pimple application, if $app or $container are passed into a closure or are provided as a function argument, the code is probably lazy.

Let's look at a more complex example that also shows the rule listed above in action:

<?php

// This is a BAD example.
$app['some.service']->addThing($app['another.service']);

// This is a GOOD example. It accomplishes the same thing as
// the bad example but in a way that is lazy.
$app['some.service'] = $app->share(
    $app->extend(
        'some.service',
        function($someService, $app) {
            // Note: we are in a closure and $app was passed
            // to the function; an indicator that this is
            // properly lazy code!
            $someService->addThing($app['another.service']);

            return $someService;
        }
    )
);

Why is the first example bad? As soon as this code is executed, both some.service and another.service services will be instantiated. Every time the application is run. This is not lazy.

The second example is better because the call to addThing on the some.service will only happen the first time some.service is requested. In this case, neither some.service nor another.service will be instantiated when this code is run. This makes both of these services lazy!

You can see the closure rule listed above in action. In the second example, $app is passed to the closure as a function argument. In the first example, this is not the case.

Laziness is very important when writing a service provider, so keep it in mind as you read about the register() and boot() methods. You can split your code up correctly but you if you get the laziness wrong you can still run into problems.

The register() Method

The purpose of the register() method is to configure the service container. In practical terms, this means setting parameters, defining new services, or extending existing services. Thats it!

For all intents and purposes, it is better to think of the method signature for register() to be:

public function register(\Pimple $container)
{
    // Define services here.
}

If you can't do it with Pimple, you probably shouldn't be doing it in register()! So it is best to forget about anything else you might otherwise do with Application, like get(), post(), mount(), before(), etc.

So, again, the only things that should be done in register() are setting parameters, defining new services, or extending existing services.

This will become more obvious in the future when Pimple service providers become a reality. At that point, the method signature for register() will actually be similar to what was written above, it will type hint Pimple. Until then, the Silex ServiceProviderInterface is going to continue to type hint Silex Application as an argument to register(). Don't let this fool you! You've been armed with knowledge! You should not actually be doing anything specific to Application in register().

The boot() Method

The boot() method is called after all of the service providers have registered themselves and after any inline services have been defined. The documentation for ServiceProviderInterface states that boot() can be used for "dynamic" configuration using services.

This is also a safe place to tie into Application specific methods. For example a service provider could choose to mount() a controller provider or it could choose to register a middleware like before() or after() from the boot() method.

While it is technically considered safe to access services from the service container at this point it is a good idea to keep in mind that services used in boot() will be instantiated for every request.

Conclusion

Service providers are invaluable in helping to modularize a Silex application but writing them incorrectly can be the source of many headaches and hidden performance problems. Hopefully this will help prevent some of the more easily avoidable mistakes.

These are somewhat tricky topics and can easily trip up even people who have been working with Silex for awhile. Have questions? There is quite a cool community brewing in #silex-php on Freenode. There are often people there willing to help out!