Wednesday 22 April 2015

PHP: Service Providers with Silex

G'day:
I'm still being obstructed by train delays... I'm now sitting at the station awaiting a train to Leytonstone, at which I can apparently change trains to continue my journey - albeit at snail's pace - into work. I'm hoping I get there in time for the Sandwich Man doing his rounds at 11am.

Anyway, recently I've got myself up to speed with the notion of service providers, in Silex. Silex is the framework we use for the reimplementation of the HostelBookers.com website. Previously I wrote an article ("PHP: messing around with Silex, Pimple & Guzzle") about how we were using Pimple in Silex to handle our dependency injection. At that time we were hand-rolling our usage of it, thus:



<?php
// app.php

use Silex\Application;

require_once __DIR__.'/../vendor/autoload.php';


$app = new Application();
$app["debug"] = true;

$app->register(new Silex\Provider\ServiceControllerServiceProvider());
$app->register(new Silex\Provider\TwigServiceProvider(), [
    "twig.path" => __DIR__.'/../src/views'
]);

app\Dependencies::configure($app);
app\Routes::configure($app);

$app->run();

Here Dependencies handles the Pimple stuff:

<?php
// Dependencies.php

namespace app;

use \dac\silexdemo\controllers;
use \dac\silexdemo\services;
use \dac\silexdemo\beans;
use GuzzleHttp\Client;

class Dependencies {

    static function configure($app){

        $app["controllers.hello"] = $app->share(function() {
            return new controllers\Hello();
        });        
        $app["controllers.user"] = $app->share(function($app) {
            return new controllers\User($app["twig"], $app["services.user"]);
        });        


        $app["services.user"] = $app->share(function($app) {
            return new services\User($app["factories.user"], $app["services.guzzle.client"]);
        });        


        $app["factories.user"] = $app->protect(function($id, $firstName, $lastName) {
            return new beans\User($id, $firstName, $lastName);
        });


        $app["services.guzzle.client"] = function() {
            return new Client();
        };
    }

}

[I've made it into work. Phew. I'll continue this later...]

This all works fine, and we ran with it for a few months. However after a while the configure() method got rather long-winded, and it was becoming a bit of a maintenance challenge, even allowing for the fact the code was pretty straight forward. We decided a refactoring exercise was in order, and whilst considering this, we had a look a how Silex's Service Providers might help.

Basically a Service Provider is the same sort of thing as we have done in Dependencies, but in a way that's designed to integrate more tightly with Silex. Silex expects the Pimple stuff to be implemented via Service Providers. So we figured we should do things as much "The Silex Way" as possible.

The conceit with a Service Provider is that it's split into two methods, as dictated by this interface:

namespace Silex;

interface ServiceProviderInterface {

    public function register(Application $app);

    public function boot(Application $app);

}

The register() method is to set the $app['whatever'] variables with their initial function expressions as per what we've been doing in Dependencies. We've broken our ones down into several different providers: based around the type of service being provided. It's slightly contrived to do so with the Dependencies class above, but we might have split that into a provider each for controllers, services and factories. In reality our divisions are different: we've got RepositoryProvider (we use the Repositiory Pattern to bridge between the data layer and the application), ModelFactoryProvider, HelperProvider (general-purpose, application-agnostic services like a StringUtilityService, ArrayUtilityService), ServiceProvider (app-specific general services) and the like.

We don't have a ControllerProvider using this model for reasons I'll go into in my next article.

For the sake of showing some code, I'll split that Dependencies class up:

<?php
namespace \dac\silexdemo\providers;

class ControllerProvider implements ServiceProviderInterface {

    use Silex\ServiceProviderInterface;
    use Silex\Application;
    use \dac\silexdemo\controllers;

    public function register(Application $app){
        $app["controllers.hello"] = $app->share(function() {
            return new controllers\Hello();
        });        
        $app["controllers.user"] = $app->share(function($app) {
            return new controllers\User($app["twig"], $app["services.user"]);
        });        
    }
    
    public function boot(Application $app){
        // no booting requirements
    }
}

<?php
namespace \dac\silexdemo\providers;

class ServiceProvider implements ServiceProviderInterface {

    use Silex\ServiceProviderInterface;
    use Silex\Application;
    use \dac\silexdemo\beans;

    public function register(Application $app){
        $app["services.user"] = $app->share(function($app) {
            return new services\User($app["factories.user"], $app["services.guzzle.client"]);
        });        

        $app["services.guzzle.client"] = function() {
            return new Client();
        };
    }
    
    public function boot(Application $app){
        // no booting requirements
    }
}

<?php
namespace \dac\silexdemo\providers;

class FactoryProvider implements ServiceProviderInterface {

    use Silex\ServiceProviderInterface;
    use Silex\Application;
    use \dac\silexdemo\services;
    use GuzzleHttp\Client;

    public function register(Application $app){
        $app["factories.user"] = $app->protect(function($id, $firstName, $lastName) {
            return new beans\User($id, $firstName, $lastName);
        });
    }
    
    public function boot(Application $app){
        // no booting requirements
    }
}

The division here is pretty arbitrary, I'll admit: the Controllers are in the ControllerProvider; general Services in the ServiceProvider and specific Factory Services defined in the FactoryProvider.

You'll notice that not one of these service providers actually implements a boot() method. Well: they have to because the interface requires it, but they have no need for it.

It took me a while to get why the boot method was "necessary", but it does make sense.

The register() method should do nothing but register potential services. It should not actually use them at this stage. It perhaps makes sense if we look at how the registration and booting come together.
  • Our Application class extends Silex's own one (SilexApp),
  • and within its constructor it calls a registerProviders() method
  • back to our Application class, we implement this registerProviders() method,
  • and it calls Silex's register() method on each of our Service Providers
  • Silex abstracts its application into two tiers; the aforementioned SilexApp, and it extends Application,
  • Within Application we actually find the register() method...
  • ... which calls the passed-in object's own register() method.

<?php
class Application extends SilexApp {

    // stuff omitted for clarity

    protected function registerProviders()
    {
        parent::registerProviders();

        $this->register(new ControllerProvider());
        $this->register(new ServiceProvider());
        $this->register(new FactoryProvider());
    }

    // etc
}

class SilexApp extends Application {

    // stuff omitted for clarity

    public function __construct()
    {
        parent::__construct();
        $this->setErrorHandler();
        $this->setMiddleWares();
        $this->registerProviders();
        $this->setRoutes();
    }

    // etc
}

<?php
class Application extends \Pimple implements HttpKernelInterface, TerminableInterface
{
    // stuff omitted for clarity

    public function register(ServiceProviderInterface $provider, array $values = array())
    {
        $this->providers[] = $provider;

        $provider->register($this);

        foreach ($values as $key => $value) {
            $this[$key] = $value;
        }

        return $this;
    }

    // etc
}

So at this juncture, the application knows about all the services that have been configured. Because the configuration has been implemented using function expressions, none of the services actually "exist" yet: they won't actually be created until they're first referenced.

This is where the division between register() and boot() comes it. register() is simply for defining what will be available as services; boot() is the first point at which one should start using them. This is because one doesn't want to start using services before all of the services are defined, because there's likely to be circular references, or one will have to define the services in a very specific sequence so that one is defined before another uses it, etc.

Note that there are plenty of cross-references in the configuration definitions, eg:

$app["services.user"] = $app->share(function($app) {
    return new services\User($app["factories.user"], $app["services.guzzle.client"]);
});

This User service will use the User Factory and the Guzzle Client Service, but because these are within the function expression, they're simply referenced; they're not actually invoked. So those services don't need to exist during the register() phase. IE: it's not until there's this sort of code:


$userService = $app["services.user"];
$something = $userService->someMethod();

... that the services.user function is called, thus creating the service. So only then does $app["factories.user"] and $app["services.guzzle.client"] need to exist. And, indeed, the references to them there will actually create the UserFactory and GuzzleClientService similar to how the first statement there creates the UserService. It's all quite slick.

So the register() process can reference other services, but it mustn't actually use any of them.
Later in the Silex bootstrap process boot() is called:

<?php
// index.php
$app = new HbApp();
$app->run();

<?php
class Application extends \Pimple implements HttpKernelInterface, TerminableInterface
{
    // stuff omitted for clarity

    public function run(Request $request = null)
    {
        // stuff omitted for clarity

        $response = $this->handle($request);
        
        // etc
    }

    public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
    {
        if (!$this->booted) {
            $this->boot();
        }

        // etc
    }

    public function boot()
    {
        if (!$this->booted) {
            foreach ($this->providers as $provider) {
                $provider->boot($this);
            }

            $this->booted = true;
        }
    }

    // etc
    
}

<?php
namespace \dac\silexdemo\providers;

class ControllerProvider implements ServiceProviderInterface {

    use Silex\ServiceProviderInterface;
    use Silex\Application;
    use \dac\silexdemo\controllers;

    public function register(Application $app){
        $app["controllers.hello"] = $app->share(function() {
            return new controllers\Hello();
        });        
        $app["controllers.user"] = $app->share(function($app) {
            return new controllers\User($app["twig"], $app["services.user"]);
        });        
    }
    
    public function boot(Application $app){
        // no booting requirements
    }
}

The sequence is:
  • our app calls run() from index.php;
  • run() calls handle();
  • handle() calls boot();
  • boot() loops through all the previously registered providers and calls their boot();
  • which is where we can start actually using the services if we needed to. In our sample code: we don't need to do anything, so they're all just stubs.
So what would we use a boot() method for?

In our app we have a CurrencyService which manages which currencies we use on the site: for displaying prices and what currencies we transact in. As this is fairly ubiquitous data, we load an array with all that info in it every request. As it's related to the service, it makes sense to have it in the provider, and we don't want to actually start using the CurrencyService in register() (register() is for registering, not doing, remember), so we do it in boot(). Similarly we use the LocaleService for getting "locale" info (subdomain language prefixes, ISO locale codes, etc). We load this stuff up in in boot() as well.

Another area where we've tripped up in the past and used these things slightly wrong is that a ServiceProvider is supposed to provide the service (as in: it registers, loads and initialises a Service object). It's not supposed to be the service. We had some providers which were actually defining functionality for the app. They should not do this, they should just provide the tools for doing the job; not do the job themselves.

I used to think Service Providers were a layer of abstraction for the hell of it. But I do see now that there's merit in having the register() / boot() process done in a uniform way.

Next up I am gonna look at Controller Providers. It took me ages to work out WTF Silex was on about with these, but I finally cracked it last Fri. Unlike Service Providers though, I remain mostly unconvinced by their merit as an approach to solving a problem. But let's see. It'll take me a day or so to write that stuff up though, so it probably won't see light of day until just before the weekend. But we'll see.

Righto.

--
Adam