SF 3.4 inject bundle config in services - symfony

Using Symfony 3.4, I just try a simple case for a new bundle (name: APiToolBundle).
Here is the content of src/ApiToolBundle/Resources/config/config.yml :
imports:
- { resource: parameters.yml }
api_tool:
api_url: %myapi_url%
api_authorization_name: 'Bearer'
This file is loaded by ApiToolBundleExtension :
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
}
I have set the Configuration file too :
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('production_tool');
$rootNode
->children()
->scalarNode('api_url')->defaultValue('')->end()
->scalarNode('api_authorization_name')->defaultValue('')->end()
->end()
;
return $treeBuilder;
}
}
But then I just want to bind a config parameter to one of my service :
# src/ApiToolBundle/Resources/config/services.yml
ApiToolBundle\Service\MyApi:
bind:
$apiUrl: '%api_tool.api_url%'
I am based on this doc : https://symfony.com/doc/3.4/bundles/configuration.html
But I am not sure to understand everything, since they talk about mergin in other way ... Do I need to do something else to load my own bundle config file ?

This is indeed a bit tricky to grasp. The bundle configuration is distinct from the parameters you use in your service configuration (even though inside your config you can also define services, which seems a bit odd at first). This is one of the reasons why Symfony in Version 4 discouraged using the semantic configuration inside applications, not use bundles & configuration, and instead directly work with parameters and services instead.
You will need to map the configuration to parameters or directly inject them into the service where you need them, in case you don't want them to be publicly available to other services or to be pulled from the container using getParameter. You can do this in the Extension where you have access to the ContainerBuilder.
See for example the FrameworkExtension where you have large configs that change the container: https://github.com/symfony/symfony/blob/v3.4.30/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php#L194-L196
In your case it could look something like this:
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
# Below is the new part, the rest is copied from your question
# If you only want to use the configuration values internally in your services:
$container->getDefinition('ApiToolBundle\Service\MyApi')->setArgument(
0 # Offset of the argument where you want to use the api url
$config['api_url']
);
# If you want to make the values publicly available as parameters:
$container->setParameter('api_tool.api_url', $config['api_url']);
}

Related

ONGR Elasticsearch Bundle AWS Connection Issue

Hello Ther i try to connenct to ES indet that is located on AWS, but i still get he Error.
[Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException]
You have requested a non-existent service "es.managers.default".
I installed the bundle using Conposer, as described on the Docs.
then added a config part tom my config.ylm
ongr_elasticsearch:
managers:
default:
index:
index_name: contents
hosts:
- https://search-***.es.amazonaws.com:443
mappings:
- StatElasticBundle
i have a awsaccesskey and a awssecretkey, but i don't now where i have to put them, so i created a aws_connection section in the parameters.yml and try to load it
Then i try to establish a connection in my SymfonyBundle and created a class in my Bundle->DepandencyInjection folder to extend my bundle, this is where i get the error. Mayby someone of you struggled with the same error?
Thanks for help.
Class StatElasticExtension extends Extension
{
const YAML_FILES = [
'parameters.yml',
'config.yml',
'services.yml'
];
/**
* {#inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
foreach (self::YAML_FILES as $yml) {
$loader->load($yml);
}
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$serviceDefinition = $container->getDefinition('es.managers.default');
$awsConnections = $container->getParameter('aws_connections');
$elasticsearchClient = $this->getClient($awsConnections);
$serviceDefinition->replaceArgument(2, $elasticsearchClient);
}
The correct service name is es.manager.default.

symfony2 recompile container from controller

I want to recompile container from controller when I use $this->container->compile();
public function changeAction(Request $request)
{
//......
echo($this->container->getParameter('mailer_user')."\n");
/*$cmd='php ../app/console cache:clear';
$process=new Process($cmd);
$process->run(function ($type, $buffer) {
if ('err' === $type) {
echo 'ERR > '.$buffer;
}
else {
echo 'OUT > '.$buffer;
}
});*/
$this->container->compile();
echo($this->container->getParameter('mailer_user')."\n");
die();
}
I got an error : You cannot compile a dumped frozen container
I want to know if when I clear the cache from controller the container will recompile?
If you are trying to get values of parameters that have been modified during request, you can do this:
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
public function changeAction(Request $request)
{
$originalParam = $this->container->getParameter('mailer_user');
// Rebuild the container
$container = new ContainerBuilder();
$fileLocator = new FileLocator($this->getParameter('kernel.root_dir').'/config');
// Load the changed config file(s)
$loader = new PhpFileLoader($container, $fileLocator);
$loader->setResolver(new LoaderResolver([$loader]));
$loader->load('parameters.php'); // The file that loads your parameters
// Get the changed parameter value
$changedParam = $container->get('mailer_user');
// Or reset the whole container
$this->container = $container;
}
Also, if you need to clear the cache from a controller, there is a cleaner way:
$kernel = $this->get('kernel');
$application = new \Symfony\Bundle\FrameworkBundle\Console\Application($kernel);
$application->setAutoExit(false);
$application->run(new \Symfony\Component\Console\Input\ArrayInput(
['command' => 'cache:clear']
));
In short the answer is no, the container will not be recompiled, because it is already loaded into memory, and deleting files from disk will take no effect on current request. And on the next request cache will be warmed up and container will be compiled before you reach the controller.

Load custom configuration in a console command using dependency-injection

I have started using Symfony's console components to build various cli tools.
I am currently slapping together such a console app, that has require various configurations, some of which are shared among commands, other configs are unique to the command.
At first I was using a helper class, with a static function call to load a regular configuration array.
Yesterday I refactored this and now load configuration in the config component, along with the treeBuilder mechanism for validation. This is all done in the main console script, not in the "command" classes.
$app = new Application('Console deployment Application', '0.0.1');
/**
* Load configuration
*/
$configDirectories = array(__DIR__.'/config');
$locator = new FileLocator($configDirectories);
$loader = new YamlConfigLoader($locator);
$configValues = $loader->load(file_get_contents($locator->locate("config.yml")));
// process configuration
$processor = new Processor();
$configuration = new Configuration();
try {
$processedConfiguration = $processor->processConfiguration(
$configuration,
$configValues
);
// configuration validated
var_dump($processedConfiguration);
} catch (Exception $e) {
// validation error
echo $e->getMessage() . PHP_EOL;
}
/**
* Load commands
*/
foreach(glob(__DIR__ . '/src/Command/*Command.php') as $FileName) {
$className = "Command\\" . rtrim(basename($FileName), ".php");
$app->addCommands(array(
new $className,
));
}
$app->run();
Currently, the only means to setup the configuration is to setup the code that loads the configuration in a separate class and call this class in in the configure() method of every method.
Maybe there is a more "symfonyish" way of doing this that I missed, I also would like to avoid having the entire framework in codebase, this is meant to be a lightweight console app.
Is there a way to pass the processed configuration to the commands being invoked, using DI or some other method I am not aware of?
Manual Injection
If you wany to keep things light and only have one (the same) configuration object for all commands, you don't even needa DI container. Simply create the commands like this:
...
$app->addCommands(array(
new $className($configuration),
));
Although you have to be aware of the trade-offs, e.g. you will have to have more effort extending this in the future or adjust to changing requirements.
Simple DI Container
You can of course use a DI container, there is a really lightweight container called Twittee, which has less than 140 characters (and thus fits in a tweet). You could simply copy and paste that and add no dependency. In your case this may end up looking similar to:
$c = new Container();
$c->configA = function ($c) {
return new ConfigA();
};
$c->commandA = function($c) {
return new CommandA($c->configA());
}
// ...
You then would need to set that up for all your commands and configurations and then simply for each command:
$app->addCommand($c->commandA());
Interface Injection
You could roll your own simple injection mechanism using interfaces and setter injection. For each dependency you want to inject you will need to define an interface:
interface ConfigAAwareInterface {
public function setConfigA(ConfigA $config);
}
interface ConfigBAwareInterface {
public function setConfigA(ConfigA $config);
}
Any class that needs the dependency can simply implement the interface. As you will mostly repeat the setters, make use of a trait:
trait ConfigAAwareTrait {
private $config;
public function setConfigA(ConfigA $config) { $this->config = $config; }
public function getConfigA() { return $this->config }
}
class MyCommand extends Command implements ConfigAAwareInterface {
use ConfigAAwareTrait;
public function execute($in, $out) {
// access config
$this->getConfigA();
}
}
Now all that is left is to actually instantiate the commands and inject the dependencies. You can use the following simple "injector class":
class Injector {
private $injectors = array();
public function addInjector(callable $injector) {
$this->injectors[] = $injector;
}
public function inject($object) {
// here we'll just call the injector callables
foreach ($this->injectors as $inject) {
$inject($object);
}
return $object;
}
}
$injector = new Injector();
$configA = new ConfigA();
$injector->addInjector(function($object) use ($configA) {
if ($object instanceof ConfigAAwareInterface) {
$object->setConfigA($configA);
}
});
// ... add more injectors
Now to actually construct a command, you can simply call:
$injector->inject(new CommandA());
And the injector will inject dependencies based on the implemented interfaces.
This may at first seem a little complicated, but it is in fact quite helpful at times.
However, if you have multiple objects of the same class that you need to inject (e.g. new Config("path/to/a.cfg") and new Config("path/to/b.cfg")) this might not be an ideal solution, as you can only distinguish by interfaces.
Dependency Injection Library
You can of course also use a whole library and add that as dependency. I have written a list of PHP dependency injection containers in a separate answer.

Alter service (ClientManager) based on configuration inside bundle extension class

I have a bundle named: "ApiBundle". In this bundle I have the class "ServiceManager", this class is responsible for retrieving a specific Service object. Those Service objects needs to be created based on some configuration, so after this piece of code in my bundle extension class:
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
// Create Service objects...
I create those Service objects right after I have processed the configuration, something like this:
foreach ($services as $name => $service) {
$service = new Service();
$service->setName($name);
$manager = $container->get($this->getAlias() . '.service_manager');
$manager->add($service);
}
Unfortunately, this does not work, probably because the container isn't compiled yet. So I tried to add those Service objects the following way:
$manager = $container->getDefinition($this->getAlias() . '.service_manager');
$manager->addMethodCall('add', array($service));
But again, this throws the following exception: RuntimeException: Unable to dump a service container if a parameter is an object or a resource.
I can't seem to get a grasp on how to use the service container correctly. Does someone knows how I can add those Service objects to the ServiceManager (which is a service) inside the bundle extension class?
This is how the configuration of the bundle looks like:
api_client:
services:
some_api:
endpoint: http://api.yahoo.com
some_other_api:
endpoint: http://api.google.com
Every 'service' will be a seperate Service object.
I hope I explained it well enough, my apologies if my english is incorrect.
Steffen
EDIT
I think I may have solved the problem, I made a Compiler Pass to manipulate the container there with the following:
public function process(ContainerBuilder $container)
{
$services = $container->getParameter('mango_api.services');
foreach ($services as $name => $service) {
$clientManager = $container->getDefinition('mango_api.client_manager');
$client = new Definition('Mango\Bundle\ApiBundle\Client\Client', array($name, 'client', 'secret'));
$container->setDefinition('mango_api.client.' .$name, $client);
$clientManager->addMethodCall('add', array($client));
}
}
Is this appropriate?
To create services based on configuration you need to create compiler pass and enable it.
Compiler passes give you an opportunity to manipulate other service
definitions that have been registered with the service container.
I think I may have solved the problem, I made a Compiler Pass to manipulate the container there with the following:
public function process(ContainerBuilder $container)
{
$services = $container->getParameter('mango_api.services');
foreach ($services as $name => $service) {
$clientManager = $container->getDefinition('mango_api.client_manager');
$client = new Definition('Mango\Bundle\ApiBundle\Client\Client', array($name, 'client', 'secret'));
$client->setPublic(false);
$container->setDefinition('mango_api.client.' .$name, $client);
$clientManager->addMethodCall('add', array($client));
}
}

Optional parameter dependency for a service

I know that i is possible to add optional service dependency for a service. The syntax is
arguments: [#?my_mailer]
But how do i add optional parameter dependency for a service?
arguments: [%my_parameter%]
I tried
arguments: [%?my_parameter%]
arguments: [?%my_parameter%]
But neither of them work, is this feature implemented in sf2?
From Symfony 2.4 you can use expression for this:
arguments: ["#=container.hasParameter('some_param') ?
parameter('some_param') : 'default_value'"]
More at http://symfony.com/doc/current/book/service_container.html#using-the-expression-language
I think that if you don't pass/set the parameter, Symfony will complain about the service dependency. You want to make the parameter optional so that it is not required to always set in the config.yml file. And you want to use that parameter whenever it is set.
There is my solution:
# src/Acme/HelloBundle/Resources/config/services.yml
parameters:
my_parameter:
services:
my_mailer:
class: "%my_mailer.class%"
arguments: ["%my_parameter%"]
And then
# you-bundle-dir/DependencyInjection/Configuration.php
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('you_bundle_ns');
// This is for wkhtmltopdf configuration
$rootNode
->children()
->scalarNode('my_parameter')->defaultNull()->end()
->end();
// Here you should define the parameters that are allowed to
// configure your bundle. See the documentation linked above for
// more information on that topic.
return $treeBuilder;
}
And then
# you-bundle-dir/DependencyInjection/YourBundleExtension.php
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('services.xml');
$container->setParameter(
'you_bundle_ns.your_parameter',
isset($$config['you_bundle_ns']['your_parameter'])?$$config['you_bundle_ns']['your_parameter']:null
);
}
You make the your parameter optional by giving default value to the '%parameter%'
Please let me know if you have better alternatives.
Did you try to set a default value for a parameter? Like so:
namespace Acme\FooBundle\Services;
class BarService
{
public function __construct($param = null)
{
// Your login
}
}
and not injecting anything.

Resources