Silex: creating a service provider for Buzz

Posted on by Matthias Noback

The class Silex\Application extends \Pimple, a very lean dependency injection container, which itself implements \ArrayAccess. This allows you to define parameters and services and retrieve them from the Application like keys in an array. The first time I defined a service in a Silex application, in my case the Buzz Browser, I did it "inline", i.e. inside my app.php file. This is how I did it:

use Buzz\Browser;

$app = new Silex\Application();

$app['autoloader']->registerNamespace('Buzz', __DIR__.'/vendor/buzz/lib');

$app['buzz.client_class'] = 'Buzz\\Client\\Curl';

$app['browser'] = $app->share(function() use ($app) {
    $clientClass = $app['buzz.client_class'];
    return new Browser(new $clientClass);
});

// $app['browser'] is ready for use

But this is not very reusable; every Silex application that needs Buzz\Browser as a service, needs to take care of the autoloading, configure and define the service. The Browser service is not such a very complex service, but think about everything that needs to be done to define and configure a service like the Doctrine DBAL (of course, Silex has a DoctrineServiceProvider for that already...).

For reusability, Silex allows you to register a service provider. A service provider receives the Silex\Application as it's only argument, so the provider can attach services to it. Then, in your app.php file, you only have to register the service provider, and then the services that are defined by the service provider are available in your application:

$app->register(new SomeServiceProvider(), array('some_service.some_argument' => 'some-value'));

Silex\Application adds the array with arguments as parameters to itself, so inside the service provider you can use them to configure your service:

use Silex\ServiceProviderInterface;
use Silex\Application;

use Matthias\SomeService;

class SomeServiceProvider implements ServiceProviderInterface
{
    public function register(Application $app)
    {
        if (!isset($app['some_service.some_argument'])) {
            $argument = 'default-value;
        }
        else {
            $argument = $app['some_service.some_argument'];
        }

        $app['some_service'] = $app->share(function() use ($app) {
            return new SomeService($argument);
        });
    }
}

In the case of the Buzz Browser, it's constructor requires a client which implements Buzz\Client\ClientInterface. Buzz for example provides clients for cURL or file_get_contents. I want to make the client configurable through service parameters. Furthermore, I also want to be able to define a custom class (e.g. for my own client, which caches responses, or does not make real requests at all). This is how I did it:

namespace Matthias\ServiceProvider;

use Silex\ServiceProviderInterface;
use Silex\Application;

use Buzz\Browser;
use Buzz\Client\ClientInterface;

class BuzzServiceProvider implements ServiceProviderInterface
{
    public function register(Application $app)
    {
        // add the path to the autoloader
        if (isset($app['buzz.path'])) {
            $app['autoloader']->registerNamespace('Buzz', $app['buzz.path']);
        }

        // provides shortcut for choosing a client: can be "curl" or "filegetcontents"
        if (isset($app['buzz.client'])) {
            $client = $app['buzz.client'];
            $clientClass = '\\Buzz\\Client\\'.$client;
            if (!class_exists($clientClass)) {

                throw new \InvalidArgumentException(sprintf('Buzz client "%s" not found', $client));
            }

            $app['buzz.client_class'] = $clientClass;
        }

        // otherwise, use a custom class, if provided
        if (!isset($app['buzz.client_class'])) {
            throw new \InvalidArgumentException('Provide a client class for Buzz');
        }

        $clientClass = $app['buzz.client_class'];
        $client = new $clientClass();

        // check if the client is valid
        if (!($client instanceof \Buzz\Client\ClientInterface)) {
            throw new \InvalidArgumentException('The client should implement Buzz\\Client\\ClientInterface');
        }

        // add the Browser as a shared service
        $app['browser'] = $app->share(function() use ($app, $client) {
            return new Browser($client);
        });
    }
}

Add this to app.php to register the BuzzServiceProvider (make sure the Buzz files can indeed be found in the specified directory).

$app->register(new BuzzServiceProvider(), array(
    'buzz.path' => __DIR__.'/vendor/buzz/lib',
    'buzz.client' => 'filegetcontents',
));

Finally, this is how you can use the browser:

use Matthias\ServiceProvider\BuzzServiceProvider;
use Symfony\Component\HttpFoundation\Response;

// ...

$app->get('/', function() use ($app) {
    $result = $app['browser']->get('http://php-and-symfony.matthiasnoback.nl/');

    return new Response($result->getReasonPhrase(), $result->getStatusCode());
});
PHP Silex dependency injection Buzz reusability