DEV Community

Nacho Colomina Torregrosa
Nacho Colomina Torregrosa

Posted on • Edited on

Create a service on the fly on a symfony third party bundle

When developing a symfony thrid party bundle, we could need to create services based on certain conditions. In this case, we'll have to achieve it using our bundle extension class instead of defining service on the services file.

Let's see a simple example. Imagine we are creating a bundle which can connect to redis using many ways depending on its configuration:

  • Case 1: Bundle configuration defines host and port
  • Case 2: Bundle configuration defines uri connection
  • Case 3: Bundle configuration defines redis sock path

Following the cases we've just defined, we're going to create a service which will connect to redis taking account them. Our service will look like this:

class RedisWrapper {

    public function __construct(
        public readonly Predis\Client $client
    ){ }

    // .......
}
Enter fullscreen mode Exit fullscreen mode

The configuration class

The configuration class defines the bundle configuration parameters using the Symfony\Component\Config\Definition\Builder\TreeBuilder class. It remains under de DependencyInjection folder which must be located in your bundle root dir.

Let's see how it looks like:


use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class MyBundleConfiguration implements ConfigurationInterface
{

    public function getConfigTreeBuilder(): TreeBuilder
    {
        $tb = new TreeBuilder('ict_api_one_endpoint');

        $tb
            ->getRootNode()
            ->children()
            ->scalarNode('host')
                ->defaultNull()
            ->end()
            ->scalarNode('port')
                ->defaultValue(6379)
            ->end()
            ->scalarNode('uri')
                ->defaultNull()
            ->end()
            ->scalarNode('sock_path')
                ->defaultNull()
            ->end()
        ;
        return $tb;
   }
}
Enter fullscreen mode Exit fullscreen mode

Method getConfigTreeBuilder returns the bundle configuration as a TreeBuilder object. As we can see, all parameters are optional. The configuration for each of the cases would be the following:

Case 1

my_bundle_name:
   host: 195.230.65.145
   port: 6379
Enter fullscreen mode Exit fullscreen mode

Case 2

my_bundle_name:
   uri: 'tcp://195.230.65.145:6379'
Enter fullscreen mode Exit fullscreen mode

Case 3

my_bundle_name:
   sock_path: '/path/to/my.sock'
Enter fullscreen mode Exit fullscreen mode

You can explore more about configurations here

Now, its time to create the service according to the last possible configs.

The extension file

The extension file loads the services and parameters which bundle exposes to projects where it is installed. As configuration class, extension class also remains under DependencyInjection folder.

Normally it starts by loading the container using the bundle services file (normally located under Resources/config folder) and then loading the configuration according to the configured parameters values.

Let's see how it looks like

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference;

class IctApiOneEndpointExtension extends Extension
{

    /**
     * @throws \Exception
     */
    public function load(array $configs, ContainerBuilder $container): void
    {
        $loader = new XmlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config'));
        $loader->load('services.xml');

        $configuration = new IctApiOneEndpointConfiguration();
        $config = $this->processConfiguration($configuration, $configs);

        if(!empty($config['host']) && !empty($config['port'])) {
            $container
                ->register('my.redis_client', Predis\Client::class)
                ->addArgument(['scheme' => 'tcp', 'host' => $config['host'], 'port' => $config['port']])
            ;
        }
        else if(!empty($config['uri'])) {
            $container
                ->register('my.redis_client', Predis\Client::class)
                ->addArgument($config['uri'])
            ;
        }
        else{
            if(empty($config['sock_path'])) {
                throw new \RuntimeException('Missing arguments for loading bundle');
            }

            $container
                ->register('my.redis_client', Predis\Client::class)
                ->addArgument(['scheme' => 'unix', 'host' => $config['sock_path']])
            ;
        }

        $container
            ->register('my.redis_wrapper', RedisWrapper::class)
            ->addArgument(new Reference('my.redis_client'))
        ;

    }
}
Enter fullscreen mode Exit fullscreen mode

Let's explore extension code step by step:

$loader = new XmlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config'));
$loader->load('services.xml');

$configuration = new IctApiOneEndpointConfiguration();
$config = $this->processConfiguration($configuration, $configs);
Enter fullscreen mode Exit fullscreen mode

This is as all extension classes usually start. It simply loads container from xml or yaml services file. Then, it processes bundle configuration from the project installer configured values.

if(!empty($config['host']) && !empty($config['port'])) {
   $container
      ->register('my.redis_client', Predis\Client::class)
      ->addArgument(['scheme' => 'tcp', 'host' => $config['host'], 'port' => $config['port']])
   ;
}
Enter fullscreen mode Exit fullscreen mode

If host and port parameters are not empty, it registers a service from Predis\Client class passing as an argument an array with:

  • scheme: Normally tcp
  • host: Value of host configuration parameter
  • port: Value of port configuration parameter
else if(!empty($config['uri'])) {
    $container
       ->register('my.redis_client', Predis\Client::class)
       ->addArgument($config['uri'])
    ;
}
Enter fullscreen mode Exit fullscreen mode

If uri parameter is not empty, then we register the same service but passing as an argument the redis uri connection.

else{
    if(empty($config['sock_path'])) {
        throw new \RuntimeException('Missing arguments for loading bundle');
    }

    $container
       ->register('my.redis_client', Predis\Client::class)
       ->addArgument(['scheme' => 'unix', 'host' => $config['sock_path']])
    ;
}
Enter fullscreen mode Exit fullscreen mode

As last option, if sock_path parameter is empty a \RuntimeExeption is thrown since there are no more options by which register the connection. Otherwise, we register the service but passing as an argument an array with:

  • scheme: unix
  • host: sock_path value
$container
    ->register('my.redis_wrapper', RedisWrapper::class)
    ->addArgument(new Reference('my.redis_client'))
;
Enter fullscreen mode Exit fullscreen mode

Finally, we register the RedisWrapper service adding as an argument a Reference to my.redis_client

Thus, we allow developers to configure this bundle giving then 3 options:

  • Using a host and a port
  • Using a uri connection
  • Using a sock path

If you read predis docs you will see at "Connecting to Redis" section that Predis\Client class can accept as an argument the three cases we've just seen in this post.

You can see a similar case in my secrets bundle

If you enjoy my content and like the Symfony framework, consider reading my book: Building an Operation-Orie8nted Api using PHP and the Symfony Framework: A step-by-step guide

Top comments (0)