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
){ }
// .......
}
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;
}
}
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
Case 2
my_bundle_name:
uri: 'tcp://195.230.65.145:6379'
Case 3
my_bundle_name:
sock_path: '/path/to/my.sock'
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'))
;
}
}
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);
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']])
;
}
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'])
;
}
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']])
;
}
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'))
;
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