Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Bundle Configuration Class

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $8.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

The KnpUIpsum class has two constructor args, but the user can't control these... yet. In knpu_lorem_ipsum.yaml, here's my idea: allow the user to use two new config keys, like unicorns_are_real and min_sunshine, and pass those values to our service as arguments.

Comment-out the var_dump. Symfony's configuration system is smart: all the keys are validated. If you typo a key - like secret2 under framework, when you refresh, you get a big ol' error! Yep, each bundle creates its own "tree" of all the valid config keys.

In fact, find your terminal. Run:

php bin/console config:dump framework

This is an example of the entire tree of valid configuration for framework! This is amazing, and it's made possible by a special Configuration class. It's time to create our own!

Creating the Configuration Class

Inside the DependencyInjection directory, create a new class called Configuration. Make this implement ConfigurationInterface: the one from the Config component. We'll need to implement one method: go to the Code -> Generate menu, or Cmd+N on a Mac, select "Implement Methods" and choose getConfigTreeBuilder().

... lines 1 - 6
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
}
}

This is one of the strangest classes you'll ever see. By using PHP code, we're going to define the entire tree of valid config that can be passed to our bundle.

A great way to see how this class works is to look at an existing one! Type Shift+Shift to open a class called FrameworkExtension, deep in the core of Symfony. Yep, this is the extension class for FrameworkBundle! It has the same load() method as our extension.

In the same directory, if you click on the top tree, you'll find a class called Configuration. Inside, it defines all of the valid config keys with a, sort of, nested tree format. This is a super powerful and, honestly, super complex system. We're only going to use a few basic features. If you need to define a more complex config tree, definitely steal, um, borrow, from these core classes.

Building the Config Tree

Back in our class, start with $treeBuilder = new TreeBuilder(). Then, $rootNode = $treeBuilder->root() and pass the name of our key: knpu_lorem_ipsum.

... lines 1 - 9
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('knpu_lorem_ipsum');
... lines 14 - 21
}

Tip

Since Symfony 4.3 you should pass the root node name to the TreeBuilder instead:

$treeBuilder = new TreeBuilder('knpu_lorem_ipsum');
$rootNode = $treeBuilder->getRootNode();
// ...

Now... just start building the config tree! $rootNode->children(), and below, let's create two keys. The first will be for the "unicorns are real" value, and it should be a boolean. To add that, say ->booleanNode('unicorns_are_real'), ->defaultTrue() and to finish configuring this node, ->end().

... lines 1 - 9
public function getConfigTreeBuilder()
{
... lines 12 - 13
$rootNode
->children()
->booleanNode('unicorns_are_real')->defaultTrue()->end()
... lines 17 - 21
}

The other option will an integer: ->integerNode('min_sunshine'), default it to 3, then ->end(). Call ->end() one more time to finish the children().

... lines 1 - 13
$rootNode
->children()
... line 16
->integerNode('min_sunshine')->defaultValue(3)->end()
->end()
;
... lines 20 - 23

Weird, right!? Return the $treeBuilder at the bottom.

... lines 1 - 9
public function getConfigTreeBuilder()
{
... lines 12 - 20
return $treeBuilder;
}

Using the Configuration Class

In our extension, we can use this to validate and merge all the config together. Start with $configuration = $this->getConfiguration() and pass this $configs and the container. This simply instantiates the Configuration class.

... lines 1 - 9
class KnpULoremIpsumExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
... lines 14 - 16
$configuration = $this->getConfiguration($configs, $container);
... lines 18 - 19
}
... lines 21 - 25
}

Here's the really important part: $config = $this->processConfiguration(): pass the configuration object and the original, raw array of $configs. var_dump() that final config and die!

... lines 1 - 11
public function load(array $configs, ContainerBuilder $container)
{
... lines 14 - 17
$config = $this->processConfiguration($configuration, $configs);
var_dump($config);die;
}
... lines 21 - 27

Let's see what happens! Find your browser and... refresh! We get an error... which is awesome! It says:

Unrecognized option "bar" under "knpu_lorem_ipsum"

This is telling us:

Yo! "bar" is not one of the valid config keys!

Back in knpu_lorem_ipsum.yaml, temporarily comment-out all of our config. And, refresh again. Yes! No error! Instead, we see the final, validated & normalized config, with the two keys we created in the Configuration class.

#knpu_lorem_ipsum:
# bar: true

Put back the config, but use a real value: min_sunshine set to 5.

knpu_lorem_ipsum:
min_sunshine: 5

Refresh one last time. Woohoo! min_sunshine equals 5. These Configuration classes are strange... but they take care of everything: validating, merging and applying default values.

Dynamically Setting the Arguments

We are finally ready to use this config. But... how? The service & its arguments are defined in services.xml... so we can't just magically reference those dynamic config values here.

Copy the service id and go back to the extension class. That container builder holds the instructions on how to instantiate our service - like its class and what constructor arguments to pass to it. And we - right here in PHP - can change those.

Check it out: start with $definition = $container->getDefinition() and pass the service id. This returns a Definition object, which holds the service's class name, arguments and a bunch of other stuff. Now we can say $definition->setArgument(): set the first argument - which is index 0 - to $config['']. The first argument is $unicornsAreReal. So use the unicorns_are_real key. Set the second argument - index one - to min_sunshine.

... lines 1 - 9
class KnpULoremIpsumExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
... lines 14 - 19
$definition = $container->getDefinition('knpu_lorem_ipsum.knpu_ipsum');
$definition->setArgument(0, $config['unicorns_are_real']);
$definition->setArgument(1, $config['min_sunshine']);
}
... lines 24 - 28
}

That's it! Go back and refresh! It works! Sunshine now appears at least 5 times in every paragraph. Our dynamic value is being passed!

Oh, and, bonus! In your terminal, run config:dump again, but this time pass it knpu_lorem_ipsum:

php bin/console config:dump knpu_lorem_ipsum

Yes! Our bundle now prints its config thanks to the Configuration class. If you want to get really fancy - which of course we do - you can add documentation there as well. Add ->info() and pass a short description about why you would set this. Do the same for min_sunshine.

... lines 1 - 7
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
... lines 12 - 13
$rootNode
->children()
->booleanNode('unicorns_are_real')->defaultTrue()->info('Whether or not you believe in unicorns')->end()
->integerNode('min_sunshine')->defaultValue(3)->info('How much do you like sunshine?')->end()
... lines 18 - 21
}
}

Run config:dump again:

php bin/console config:dump knpu_lorem_ipsum

Pretty, freakin' cool.

Next, let's get fancier with our config and allow entire services to be swapped out.

Leave a comment!

9
Login or Register to join the conversation
Default user avatar
Default user avatar Pierre | posted 4 years ago | edited

Hi,
There is a deprecation on the root node (<blockquote>@deprecated since Symfony 4.3, pass the root name to the constructor instead</blockquote>), it should be :
<br />$treeBuilder = new TreeBuilder('knpu_lorem_ipsum');<br />$rootNode = $treeBuilder->getRootNode();<br />

Thanks for this great tutorial !

Pierre

1 Reply
Boolean T. Avatar
Boolean T. Avatar Boolean T. | posted 2 years ago | edited

Hi!
I should also change <i>"services.xml"</i> for everything to work. Namely - add 2 <argument /> for unicorns_are_real and min_sunshine respectively. Without it I got OutOfBoundsException: "Cannot replace arguments if none have been configured yet." (Symfony version - 5.2.1):


<services>
        <service id="knpu_lorem_ipsum.knpu_ipsum" class="KnpU\LoremIpsumBundle\KnpUIpsum" public="true">
               <argument /> <!-- unicorns_are_real -->
               <argument /> <!-- min_sunshine -->
        </service>
        <service id="KnpU\LoremIpsumBundle\KnpUIpsum" alias="knpu_lorem_ipsum.knpu_ipsum" public="false" />
 </services>
Reply
Renaud G. Avatar
Renaud G. Avatar Renaud G. | posted 3 years ago | edited

Hi!

Tried to pass a Monolog alias as a parameter just to test if I was understanding everything and I got the error :
$definition->setArgument(0, $config['logger']);
<blockquote>Argument 1 passed to TestBundle::__construct() must implement interface Psr\Log\LoggerInterface or be null, string given</blockquote>

I fiddled around for some time and solved it that way:
$definition->setArgument(0, $config['logger'] ? new Reference($config['logger']) : null);

But is this canonical? It feels weird that in the config file the alias is not prefixed with an "@":
`
knpu_lorem_ipsum:

    logger: 'monolog.logger.slack'

`

Thanks!

Edit: Okay so the next chapter kinda answers all that. Still confused about the fact that we don't need to use an "@" in the alias though.

Reply

Hey Renaud G.!

Yea, this whole "why do I need an @ here but not there" is a valid question. There is a perfectly logical reason... but it's still one of those things that you need to explain :p.

The reason is this: when you are defining a service (i.e. something under the services: key), Symfony needed some way to differentiate between you setting an argument to the string logger and the service logger. Simple enough! We'll use @logger. So far so good, right :).

Now that you're creating your shiny new bundle, you decide to add a logger key (exactly like in your code above) and YOU (the bundle author) knows that this will be a service. So... why force the user to say @logger? If you did, you would be responsible to strip off the starting @ so that you could create the new Reference without it. And if the user did not include an @, you'd have to print an error like "Even though I know that what you are passing me is the id of a service, please put an @ in front anyways".

So... like I said, it's logical why bundle authors would not force an @ to be used... but it ultimately means that your config files look a bit inconsistent. Honestly, I hope that someday (it's possible now, just not the default), we'll use PHP for the services config, which doesn't need the @ special symbol.

Cheers!

Reply
Renaud G. Avatar

Hi Ryan!

Thanks for your explanation, on point as usual!

If it really is an inconsistency, the Reference class constructor could take care of stripping the "@" only if present? Right now I'm doing ltrim($service_id, '@') so I can use both.

Also tried to use $container->getAlias($config['name']) but the alias for monolog was unlinkable for some reason. I guess it's not supposed to be used that way ^^

Reply

Hey Renaud G.!

> If it really is an inconsistency, the Reference class constructor could take care of stripping the "@" only if present? Right now I'm doing ltrim($service_id, '@') so I can use both.

You could definitely make this argument :).

> Also tried to use $container->getAlias($config['name']) but the alias for monolog was unlinkable for some reason. I guess it's not supposed to be used that way ^^

When you're in your *BundleExtension class, the ContainerBuilder that you're passed is an *empty* ContainerBuilder. This is done on purpose: Symfony is kind of "sandboxing" you: you can add services, etc, but you don't have access to read or change any services from any other bundles. When your method is done, Symfony merges your ContainerBuilder into the real one :). That's why you can't access other services or aliases. This is also done because if you *could* reference services/aliases from other bundles, ordering would be a mess: what if your bundle is (in some apps) loaded *before* MonologBundle? It keeps you away from that ugliness ;).

Cheers!

Reply
Default user avatar

Hey Guys,

Great tutorial!

Just a quick one. Let's assume I did all the things correctly(however I am doing that in my own project) - is there any reason why config:dump would show me ~ instead of value for specific config?

I have one key in my config - name, which is a string. I have hooked up all of that as described and all works fine. However when I am trying to double check that on console I get this:

rob_simple_test:
name: ~

Instead of the value from config file. I know for a fact that value is in $config['name'] because I can see it there right before I go and do:
$definition->setArgument(0, $config['name']);

Do you Guys have at least an idea of what might be going on here?

Unicorns and rainbows on me for the one who solves that mistery!

Regards,
Rob

Reply

Hey Robert!

It sounds like you've got all the hard stuff set up. And... yea, I think I know the answer here :). There are 2 separate commands:

  • config:dump Returns a YAML representation of all of the config keys and their default values by reading your Configuration object. This does not reflect your REAL config values in any way
  • debug:config Returns a YAML representation of the ACTUAL config values in your project (which is of course a combination of what you've configured plus any defaults from the Configuration class

In other words - this is expected! Just use debug:config instead - it should print what you're expecting.

Let me know if this answers your question! I gotta cash in on the unicorns and rainbows you promised ;).

Cheers!

Reply
Default user avatar
Default user avatar Robert | weaverryan | posted 4 years ago | edited

Hey weaverryan !

That's it. You helped a lot. Thank you for this, now it's clear.

As for the Rainbows and Unicorns - is there any KnpU Mailbox to which I can send postcard full of those magnificent things?

7 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial is built using Symfony 4, but most of the concepts apply fine to Symfony 5!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "doctrine/annotations": "^1.8", // v1.8.0
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knpuniversity/lorem-ipsum-bundle": "*@dev", // dev-master
        "nexylan/slack-bundle": "^2.0,<2.2", // v2.0.1
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.1.6
        "symfony/asset": "^4.0", // v4.0.6
        "symfony/console": "^4.0", // v4.0.6
        "symfony/flex": "^1.0", // v1.18.7
        "symfony/framework-bundle": "^4.0", // v4.0.6
        "symfony/lts": "^4@dev", // dev-master
        "symfony/twig-bundle": "^4.0", // v4.0.6
        "symfony/web-server-bundle": "^4.0", // v4.0.6
        "symfony/yaml": "^4.0", // v4.0.6
        "weaverryan_test/lorem-ipsum-bundle": "^1.0" // v1.0.0
    },
    "require-dev": {
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.4
        "sensiolabs/security-checker": "^4.1", // v4.1.8
        "symfony/debug-bundle": "^3.3|^4.0", // v4.0.6
        "symfony/dotenv": "^4.0", // v4.0.6
        "symfony/maker-bundle": "^1.0", // v1.1.1
        "symfony/monolog-bundle": "^3.0", // v3.2.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.3.3
        "symfony/stopwatch": "^3.3|^4.0", // v4.0.6
        "symfony/var-dumper": "^3.3|^4.0", // v4.0.6
        "symfony/web-profiler-bundle": "^3.3|^4.0" // v4.0.6
    }
}
userVoice