I'm using Symfony 4 and there is a configuration file which needs to be injected into the command App\Command\FooCommand in the form of an array. I have a custom DI extension registered at App\Kernel::configureContainer(), which's used to validate the custom configuration file (for the convenience of the development, the config is big and going to be changed frequently during the dev). The constructor of the command is public function __construct(Foo $foo, array $config), I'm expecting the config as the second argument.
Now how do I put this config there? The documentation says about parameters, but it's not a parameter. I was thinking of changing the definition of the command and adding this argument in the Extension::load method like this:
class FooExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);
//inject the configuration into the command
$fooCmdDef = $container->getDefinition(FooCommand::class);
$fooCmdDef->addArgument($config);
}
}
but it ends up with the error
You have requested a non-existent service "App\Command\FooCommand".
However the command must have been registered as a service automatically.
What am I doing wrong and how to inject this config?
You cannot access any service in a DI extension Class because the container was not yet compiled. For your case is common to create a Compiler Pass where you will be able to retrieve the needed service and apply any modification to it.
For example you can create a parameter in the container extension on where store the configuration:
class FooExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);
//create a container parameter
$container->setParameter('your_customized_parameter_name', $config);
}
}
Then retrieve what you need in the compiler pass and then apply some modification:
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
class YourCompilerPass implements CompilerPassInterface
{
/**
* {#inheritdoc}
*/
public function process(ContainerBuilder $container)
{
# retrieve the parameter
$config = $container->getParameter('your_customized_parameter_name');
# retrieve the service
$fooCmdDef = $container->getDefinition(FooCommand::class);
# inject the configuration
$fooCmdDef->addArgument($config);
# or you can also replace an argument
$fooCmdDef->replaceArgument('$argument', $config);
}
}
Related
I get following error:
There is no extension able to load the configuration for
"upload_images" (in
"/var/www/vhosts/diabetigraph-dev/vendor/verzeilberg/upload-images/src/Resources/services.yaml").
Looked for namespace "upload_images", found "none"
This are my files:
services.yaml
services:
verzeilberg\UploadImagesBundle\Service\Rotate:
autowire: true
upload_images:
version: 100
Configuration
namespace verzeilberg\UploadImagesBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder('upload_images');
$treeBuilder->getRootNode()
->children()
->integerNode('version')->end()
->end();
return $treeBuilder;
}
}
UploadImagesExtension
namespace verzeilberg\UploadImagesBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
class UploadImagesExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources'));
$loader->load('services.yaml');
$config = $this->processConfiguration(new Configuration(), $configs);
$container->setParameter('version', $config['version']);
}
}
What am I doing wrong?
The basic answer is that your upload_images configuration needs to be moved to it's own application level config/packages file:
# app/config/packages/upload_images.yaml
upload_images:
version: 100
In your bundle, the Configuration object represents the bundle's configuration. There is no upload_images.yaml sort of file in your bundle. The object is quite powerful so you can add defaults and options and what not.
Your bundle's extension takes care of processing the final configuration and making information such as parameters available to the rest of the system:
class UploadImagesExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
// $configs is actually an array of array representing the contents of upload_files.yaml
$config = $this->processConfiguration(new Configuration(), $configs);
$container->setParameter('upload_files.version', $config['version']);
// Loading your bundle's services.yaml file is a different process which just happens to be kicked off by the loader
// Thanks to the setParameter above, %upload_files.version% can be used by services
$loader = new YamlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources'));
$loader->load('services.yaml');
}
}
It can be confusing and for me at least I had to read the docs multiple times and do quite a bit of experimenting to understand the overall process.
The confusion is one of the reasons that Symfony evolved from multiple bundles per application to one application bundle to no application bundle at all.
I might also add the Symfony uses composer recipes to simplify installing a bundle. The recipe not only adds the bundle to config/bundles.php but copies any default configuration file to config/packages as well. Unfortunately, there are additional steps required for this copy to happen (see the docs) so it's easiest to do things the old-fashion way and just tell the developer to create the config file via the bundle's README file.
I'm trying to create reusable logging bundle which can have custom formatter for log messages.
Formatter is set in the main app's config file like that:
custom_logger:
formatter: AppBundle\Services\MessageFormatter
Then in the LoggerBundle/DependencyInjection/CustomLoggerExtension.php
after i receive this configuration i'm trying to get logger service and set formatter
class CustomLoggerExtension extends ConfigurableExtension
{
public function loadInternal(array $mergedConfig, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('services.yml');
$class = $mergedConfig['formatter'];
$obj = new $class;
$container->get('custom.logger')->setFormatter($obj);
but the problem is that logger uses request stack
services:
custom.logger:
class: LoggerBundle\Services\Logger
arguments: ['#request_stack']
and so when i try to get service in loadInternal function it seems that the request stack is not initialized yet and i receive error: You have requested a non-existent service "request_stack"
What is the correct way of doing this?
EDIT (2016-04-21 21:54:11)
That's weird. Even if i remove request stack and successfully set formatter in loadInternal, it's not in the service when i'm getting it from the controller in main app.
I must be doing something really wrong :)
Ok, I think I figured it out.
When load() or as in my case loadInternal() method is called, symfony passes there completely new container which is then merged into the main one.
So if I request a service and it is created there, it's not saved anywhere but in the temporary container and is eventually destroyed (only service definitions are passed to the main one).
Since services are loaded(created) on demand, it's not right to instantiate it on bundle load.
So what you want to do instead is to configure definitions
Here is how I got it working:
use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\Config\FileLocator;
use LoggerBundle\Formatter\MessageFormatterInterface;
class LoggerBundleExtension extends ConfigurableExtension implements CompilerPassInterface
{
protected $customFormatter;
public function loadInternal(array $mergedConfig, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('services.yml');
if (array_key_exists('formatter', $mergedConfig)) {
$this->customFormatter = $mergedConfig['formatter'];
}
}
public function process(ContainerBuilder $containerBuilder)
{
if ($this->customFormatter) {
$class = $this->customFormatter;
//if formatter is service id
$serviceUsed = $containerBuilder->has($this->customFormatter);
if ($serviceUsed) {
$class = $containerBuilder->getDefinition($this->customFormatter)->getClass();
}
if (!class_exists($class) || !is_a($class, MessageFormatterInterface::class, true)) {
throw new \ErrorException('Invalid logger formatter');
}
if ($serviceUsed) {
$containerBuilder
->getDefinition('custom.logger')
->addMethodCall('setFormatter', array(new Reference($this->customFormatter)));
} else {
$containerBuilder
->getDefinition('custom.logger')
->addMethodCall('setFormatter', array(new Definition($this->customFormatter)));
}
}
}
}
In loadInternal() I check if formatter is set in main app's config and save it.
But main thing is happening in process() function (you have to implement Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface for it to run).
From the docs.
As process() is called after all extensions are loaded, it allows you to edit service definitions of other extensions as well as retrieving information about service definitions.
So, what I am doing is:
checking if formatter option value is a class name or a service id(if that's the case, I retrieve class name from service definition)
validate class name (checking if it's a valid class and making sure it implements required interface)
adding definition to my logger service which tells container to call setFormatter with provided arguments when logger service is created(when it is first retrieved from the container)
Basically
$containerBuilder->getDefinition('custom.logger')->addMethodCall('setFormatter', array(new Reference('my_formatter')));
is the same as declaring in your config:
services:
custom.logger:
class: LoggerBundle\Services\Logger
arguments: ['#request_stack']
calls:
- [setFormatter, ['#my_formatter']]
I have a bundle which was working quite well for some time. However, I had to add some custom configuration params to it, so I wrote some lines in the bundle's config.yml, something like this:
# ...
acme_my_bundle:
special_params: ['param_1', 'param_2']
The configuration is defined in the bundle's Configuration class:
namespace ACME\MyBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
* This is the class that validates and merges configuration from your app/config files
*
* To learn more see {#link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class}
*/
class Configuration implements ConfigurationInterface {
/**
* {#inheritdoc}
*/
public function getConfigTreeBuilder() {
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('acme_my_bundle');
$rootNode
->children()
->arrayNode('special_params')
->end()
->end();
return $treeBuilder;
}
}
The bundle is properly registered in AppKernel.php:
public function registerBundles() {
$bundles = array(
// ...
new ACME\MyBundle(),
// ...
);
// ...
return $bundles;
}
However, when I try to use my app, I get an error:
There is no extension able to load the configuration for "acme_my_bundle" (in (path_to_bundle)/MyBundle/DependencyInjection/../Resources/config/config.yml). Looked for namespace "acme_my_bundle", found none
I looked it up, but most results found were unsatisfactory - I eliminated the problems that came up during searching:
improper configuration structure
bundle not registered in the app kernel
config root node name different than the one returned from ACMEMyBundleExtension::getAlias()
I tried debugging the cause of the exception being thrown and discovered that when the YAML file loader tries to validate my config file, the container has no extensions:
var_dump($container->getExtensions()); // prints empty array - array(0) { }
It causes the validation to fail and the none part of the message to be displayed - there a no available extensions.
I tried debugging $this->extensions in ContainerBuilder::hasExtension() and for some reason the list is complete when the method is launched for the vendor bundles, but is empty for my bundle. It looks like that something in my bundle is still defined or registered incorrectly.
I changed the names off classes, etc. not to expose company code, excuse me for that if it causes confusion.
EDIT: I didn't explicitly mention it, but the Extension class is defined and the exception occurs when it is loaded - just as I have written above:
when the YAML file loader tries to validate my config file
To be more clear, here is my Extension class:
namespace ACME\MyBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
/**
* This is the class that loads and manages your bundle configuration
*
* To learn more see {#link http://symfony.com/doc/current/cookbook/bundles/extension.html}
*/
class ACMEMyBundleExtension extends Extension {
/**
* {#inheritdoc}
*/
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'));
// The exception is thrown here
$loader->load('config.yml');
}
}
Check your configuration reader in ACME\MyBundle\DependencyInjection\Configuration for $rootNode = $treeBuilder->root('BUNDLE_CONFIG_KEY');.
BUNDLE_CONFIG_KEY should be:
valid (same in ACME\MyBundle\DependencyInjection\Configuration and your config.yml
unique for the app
Also please check you're defining bundle configuration in correct way - it should be added to app/config/*.yml (one of global config files). Maybe you have added acme_my_bundle config in other custom bundle config files?
You have missed bundle extension class (ACME\MyBundle\DependencyInjection\ACMEMyExtension) as explained here http://symfony.com/doc/current/cookbook/bundles/extension.html. Cookbook entry for bundle configuration is here. Key in config.yml should be named only as acme_my.
Creating the Configuration class alone is not enough. You need to register a dependency injection extension and use the Configuration class in there.
Read more about in the How to Create Friendly Configuration for a Bundle cookbook:
[The Configuration] class can now be used in your load() method to merge configurations and force validation (e.g. if an additional option was passed, an exception will be thrown)
namespace Acme\MyBundle\DependencyInjection;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class AcmeMyBundleExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
// ...
}
}
Naming your extension class according to the convention will make it's automatically loaded. More on creating DIC extension classes in Creating an Extension Class. You can also enable the extension manually, see Manually registering an extension class.
Iam trying to implement service for encoding password but it seems it doesn't work cause I get "You have requested a non-existent service user_service " error.
Here is my code :
Vendor/BundleNameBundle/Resources/config/services.yml
services:
user_service:
class: Morescreens\VideomanagerBundle\Service\UserService
arguments: ['#security.encoder_factory']
app/config/config.yml
imports:
- { resource: "#morescreensVideomanagerBundle/Resources/config/services.yml" }
code for my service
class UserService {
/**
* #var EncoderFactoryInterface
*/
private $encoderFactory;
public function __construct(EncoderFactoryInterface $encoderFactory)
{
$this->encoderFactory=$encoderFactory;
}
public function setEncodedPassword(User $user)
{
$encoder=$this->encoderFactory->getEncoder($user);
$password=$encoder->encodePassword($user->getPassword(),$user->getSalt());
$user->setPassword($password);
}
}
Inside mine controller:
$user=new User();
$user->setPassword('password');
$this->get('user_service')->setEncodedPassword($user);
EDIT:
I manually deleted cache folder and my service started to work.
Try looking at your src/{VendorName}/{BundleName}Bundle/DependencyInjection/{VendorName}{BundleName}Extension.php
This file should load your service definitions like this:
<?php
namespace Vendor\{BundleName}Bundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
/**
* This is the class that loads and manages your bundle configuration
*
* To learn more see {#link http://symfony.com/doc/current/cookbook/bundles/extension.html}
*/
class Vendor{BundleName}Extension extends Extension
{
/**
* {#inheritDoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
//Load our YAML resources
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
}
}
This will load the service definition from src/{VendorName}/{BundleName}Bundle/Resources/config/services.yml into the container. If that still doesn't work try doing php app/console cache:clear. For speed reasons, symfony will basically aggregate all your service (and other configuration files) into a cached file so it doesn't have to read those files every time. The files that actually cache the service definitions are located:
app/cache/{envName}/ (The cache directory can be configured to be located wherever you want, this is just the default).
The files that have the relevent info for container service definitions are:
app{EnvName}DebugProjectContainer.php
app{EnvName}DebugProjectContainer.php.meta
app{EnvName}DebugProjectContainer.xml
I am trying to expose a custom configuration defined for my bundle which is called DecisionBundle.
I have a Decision\DecisionBundle\Resources\config\custom.yaml that looks like this:
decision:
testValue: 11
anotherTest: 12
I also have an Extension that looks like this:
namespace Decision\DecisionBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
class DecisionExtension extends Extension
{
/**
* {#inheritDoc}
*/
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');
$loader->load('custom.yml');
}
}
The file custom.yml seems to be loaded ok. Next comes the configuration interface
namespace Decision\DecisionBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
/**
* {#inheritDoc}
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('decision');
$rootNode
->children()
->scalarNode('testValue')->defaultValue('0')->end()
->scalarNode('anotherTest')->defaultValue('0')->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;
}
}
However, this results in the following:
InvalidArgumentException: There is no extension able to load the configuration for "decision" (in *****\src\Decision\DecisionBundle\DependencyInjection/../Resources/config\custom.yml). Looked for namespace "decision", found none
Can somebody point me as to what I am doing wrong? Do I need to manually register the Extension somehow? However, the Extension seems taken into account as it looks loaded from the debug info.
Thanks!
Currently you are setting parameters in your custom.yml file in the same way that you would set them in the `app/config/config.yml' file.
decision:
testValue: 11
anotherTest: 12
When you are loading a file through the configuration file then your service files (services.yml & custom.yml in your case) should have either services or parameters as the headers. To use your parameters in the custom.yml file you should lay it out like..
parameters:
decision.testValue: 11
decision.anotherTest: 12
Also, if you are putting the parameters in the app/config/config.yml file, you need to set them to actual parameters in your DecisionExtension. To do this you would need to get the data from your Configuration file and set them in the container like..
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new
FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
$loader->load('custom.yml');
$container->setParameter('decision.testValue', $config['testValue']);
$container->setParameter('decision.anotherTest', $config['anotherTest']);
This, however, will overwrite the parameters set in your custom.yml and also negate any reason for it being there.