My previous post was about finding and loading configuration files. I now continue my Symfony2 Config Component quest with a post about the way you can "semantically expose your configuration" (using the TreeBuilder). I wrote this piece to later contribute it to the Symfony documentation so feedback is very welcome!
Validate configuration values
After loading configuration values from all kinds of resources, the values and their structure can be validated using the Definition
part of the Symfony2 Config Component. Configuration values are usually expected to show some kind of hierarchy. Also, values should be of a certain type, be restricted in number or be one of a given set of values. For example, the following configuration (in Yaml) shows a clear hierarchy and some validation rules that should be applied to it (like: "the value for 'auto_connect' must be a boolean"):
auto_connect: true
default_connection: mysql
connections:
mysql:
host: localhost
driver: mysql
username: mysql_user
password: j8dsf7sdnk3w89732df9dfn3
sqlite:
host: localhost
driver: sqlite
memory: true
username: sqlite_user
password: 9d732n32ffn3287dsfskfhk
When loading multiple configuration files, it should be possible to merge and overwrite some values. Other values may not be merged and stay as they are in the first file. Also, some keys are only available, when another key has a specific value (in the sample configuration above: the "memory" key only makes sense when the "driver" key is "sqlite").
TreeBuilder: define a hierarchy of configuration values
All the rules concerning configuration values can be defined using the TreeBuilder.
A TreeBuilder
instance should be returned from a custom Configuration
class which implements the ConfigurationInterface:
namespace Acme\DatabaseConfiguration;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
class DatabaseConfiguration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('database');
// add node definitions to the root of the tree
return $treeBuilder;
}
}
Add node definitions to the tree
Variable nodes
A tree contains node definitions which can be layed out in a semantic way. This means, using indentation and the fluent notation, it is possible to reflect the real structure of the configuration values:
$rootNode
->children()
->booleanNode('auto_connect')
->defaultTrue()
->end()
->scalarNode('default_connection')
->defaultValue('default')
->end()
->end()
;
The root node itself is an array node, and has children, like the boolean node "auto_connect" and the scalar node "default_connection". In general: after defining a node, a call to end()
takes one step up in the hierarchy.
Array nodes
It is possible to add a deeper level to the hierarchy, by adding an array node. The array node itself, may have a pre-defined set of variable nodes:
$rootNode
->arrayNode('connection')
->scalarNode('driver')->end()
->scalarNode('host')->end()
->scalarNode('username')->end()
->scalarNode('password')->end()
->end()
;
Or you may define a prototype for each node inside an array node:
$rootNode
->arrayNode('connections')
->prototype('array)
->children()
->scalarNode('driver')->end()
->scalarNode('host')->end()
->scalarNode('username')->end()
->scalarNode('password')->end()
->end()
->end()
->end()
;
Array node options
Before defining the children of an array node, you can provide options like:
useAttributeAsKey()
: provide the name of a childnode, whose value should be used as the key in the resulting arrayrequiresAtLeastOneElement()
: there should be at least one element in the array (works only whenisRequired()
is also called).
An example of this:
$rootNode
->arrayNode('parameters')
->isRequired()
->requiresAtLeastOneElement()
->prototype('array')
->useAttributeAsKey('name')
->children()
->scalarNode('name')->isRequired()->end()
->scalarNode('value')->isRequired()->end()
->end()
->end()
->end()
;
Default and required values
For all node types, it is possible to define default values and replacement values in case a node has a certain value:
defaultValue()
: set a default valueisRequired()
: must be defined (but may be empty)cannotBeEmpty()
: may not contain an empty valuedefault*()
(Null, True, False): shortcut fordefaultValue()
treat*Like()
(Null, True, False): provide a replacement value in case the value is *
$rootNode
->arrayNode('connection')
->children()
->scalarNode('driver')
->isRequired()
->cannotBeEmpty()
->end()
->scalarNode('host')
->defaultValue('localhost')
->end()
->scalarNode('username')->end()
->scalarNode('password')->end()
->booleanNode('memory')
->defaultFalse()
->end()
->end()
->end()
;
Merging options
Extra options concerning the merge process may be provided. For arrays:
performNoDeepMerging()
: when the value is also defined in a second configuration array, don't try to merge an array, but overwrite it entirely
For all nodes:
cannotBeOverwritten()
: don't let other configuration arrays overwrite an existing value for this node
Validation rules
More advanced validation rules can be provided using the ExprBuilder. This builder implements a fluent interface for a well-known control structure. The builder is used for adding advanced validation rules to node definitions, like:
$rootNode
->arrayNode('connection')
->children()
->scalarNode('driver')
->isRequired()
->validate()
->ifNotInArray(array('mysql', 'sqlite', 'mssql'))
->thenInvalid('Invalid database driver "%s"')
->end()
->end()
->end()
->end()
A validation rule always has an "if" part. You can specify this part in the following ways:
ifTrue()
ifString()
ifNull()
ifArray()
ifInArray()
ifNotInArray()
A validation rule also requires a "then" part:
then()
thenEmptyArray()
thenInvalid()
thenUnset()
The only exception is of course:
always()
Usually, "then" is a closure. It's return value will be used as a new value for the node, instead of the node's original value.
Processing configuration values
The Processor
uses the tree as it was built using the TreeBuilder
to process multiple arrays of configuration values that should be merged. If any value is not of the expected type, is mandatory and yet undefined, or could not be validated in some other way, an exception will be thrown. Otherwise the result is a clean array of configuration values.
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Config\Definition\Processor;
use Acme\DatabaseConfiguration;
$config1 = Yaml::parse(__DIR__.'/src/Matthias/config/config.yml');
$config2 = Yaml::parse(__DIR__.'/src/Matthias/config/config_extra.yml');
$configs = array($config1, $config2);
$processor = new Processor;
$configuration = new DatabaseConfiguration;
$processedConfiguration = $processor->processConfiguration($configuration, $configs);
merci
Hi Matthias,
My config data is below:
checkType: url
params:
startUrl:
route: start_route_name
slugs:
step: intro
endUrl:
route: end_route_name
checkType: another_check
params:
How can I define a rule that if checkType is 'url' then params is required and must be an array of startUrl and endUrl? if checkType is 'another_check', params is not required.
I tried:
->scalarNode('checkType')
->isRequired()
->validate()
->ifInArray(array('url'))
but I did not know how to write the then() part
Thanks for your guidance.
Hi! Thnx for your topic.
My task is to load config of swiftmailer dynamicly (depends of Host). I have several configs for it in app/config/config.yml. I tried to set right config in Bundle/DependencyInjection/BundleExtension.php in method "load" like this:
$container->setParameter( 'mailer_transport', 'gmail' );
$container->setParameter( 'mailer_user', 'example@example.com' );
$container->setParameter( 'mailer_password', 'pass' );
Its good when I run it first time but than method "load" falls in cache.
Maybe you can help me. In what way should I go to fix this. Or maybe I should create dynamicly Swift_Transport class and set it to Swift_Mailer instead?
Thank you!
Since the host is variable per request, and the application configuration is fixed for any request, I think it would be a good idea to also define transport details dynamically. You could for instance create a transport factory which takes the request as a dependency.
How do you think, its a good idea or not ?
Thnx a lot.
Already fix it by creating custom Transport class and extend it from original Transport class. Made my class like service and now I can set data in __construct of my class.
class SatellitesTransport extends \Swift_Transport_EsmtpTransport.
public function __construct(\Swift_Transport_IoBuffer $buf, array $extensionHandlers, \Swift_Events_EventDispatcher $dispatcher, Satellites $satellites)
{
parent::__construct($buf, $extensionHandlers, $dispatcher);
$this->setHost( $satellites->get('email', 'host') );
$this->setPort( $satellites->get('email', 'port') );
$this->setEncryption( $satellites->get('email', 'encryption') );
$this->setAuthMode( $satellites->get('email', 'auth_mode') );
$this->setUsername( $satellites->get('email', 'username') );
$this->setPassword( $satellites->get('email', 'password') );
}
Something like this.
Is there any way to convert values inside the Tree builder?
Suppose i have values that should be represented internally as integer constants, but i prefer to use more declarative strings in configuration. Is there any way to process the strings and map them to integer constants?
Example:
Constants: pear (1), apple (2), banana (3), orange (4)
Configuration (in config.yml)
fruits: [pear, apple, banana, orange]
i want it to be compiled to
array(
'fruits' => array(1, 2, 3, 4),
);
Hi Luciano,
You might try using the
beforeNormalization()
method which lets you modify the original array of values before the date will be normalized. The validation method could then beifNotInArray()
.Good luck,
Thanks a lot Matthias!
I have i slightly different (and a bit more complex) scenario now.
I have an array (prototyped array) and i want to transform a single string value in the array to an integer constant.
Referring to my previous fruitNames/constants example I can provide the following sample code:
things:
paul:
foo: bar
bar: baz
fruit: banana
eric:
baz: bar
foo: foo
fruit: pear
jimmy:
bar: bar
fruit: orange
should be transformed to:
array(
'things' => array(
'paul' => array(
'foor' => 'bar',
'bar' => 'baz',
'fruit' => 3
),
'eric' => array(
'baz' => 'bar',
'foo' => 'foo',
'fruit' => 1
),
'jimmy' => array(
'bar' => 'bar',
'fruit' => 4
)
)
)
I tried some different combination but it turned out that the beforeNormalization is never being execute (i tried to print something both in the if and the then part, but nothing showed up). I am not sure on how the
beforeNormalization
should be used in the TreeBuilder.Just to have a reference, I did something like the following code: http://pastie.org/5360346
I spotted the problem and i've been able to make it work.
My real use case adopted default values. I was somewhat convinced that the
beforeNormalization
would also been called on unset values using the provided default value as parameter. That's not the case: that method is called only if you set explicit values. So i had to provide an already normalized default value and everything started working fine.Thanks again Matthias!
Ah, well, I have also experienced these kind of surprises many times when working with the TreeBuilder. Glad it works for you now! Enjoy,
Hi,
I have a quick query regarding simple configuration definitions. I found your page (and the corresponding cookbook article) very informative, but what it fails to do (for me at least) is connect the config treeBuilder definitions to a corresponding config annotation. As such, I am confused about one thing:
I have a bundle config in my app/config/config.yml as follows:
ivs_signup:
service_parameters:
microsoft:
client_id: 000000xxxxx
client_secret: AsCvR329ldUxxxxxxxxxxxxxxx
auth_uri: https://login.live.com/oauth20_authorize.srf
token_uri: https://login.live.com/oauth20_token.srf
redirect_uri: http://regsrv.localdomain/app_dev.php/services/callback/microsoft
My treeBuilder is as follows:
$rootNode
->children()
->arrayNode('service_parameters')
->useAttributeAsKey('name')
->prototype('array')
->children()
->scalarNode('client_id')->end()
->scalarNode('client_secret')->end()
->scalarNode('auth_uri')->end()
->scalarNode('token_uri')->end()
->scalarNode('redirect_uri')->end()
->end()
->end()
->end()
->end()
;
2 things:
Why does Symfony not care that the useAttributeAsKey('name') directive under service_parameters does not reference an existing node?
This is my config:debug from console:
ivs_signup:
service_parameters:
# Prototype
name:
client_id: ~
client_secret: ~
auth_uri: ~
token_uri: ~
redirect_uri: ~
If I remove the useAttributeByKey directive, the config:dump information loses both "prototype" and the name: node.
I don't understand if what I'm doing is correct, I just know it works. What I want to achieve is the ability to configure a bunch of service_parameters under named array keys based off the children name of service_parameters.
Cheers!
I didn't realise how bad my ability to format text in comments was, I might just try and email you.
Hi Ross, no problem, I will try to fix the formatting. Then I will see if I can answer your question.
Hello! Am I correct understood that useAttributeAsKey it is like array_flip? Thanks!
Not really. What happens is, when you have an array like:
[php]
array(
array('name' => 'primary', 'host" => 'localhost', 'driver' => 'mysql'),
array('name' => 'secondary', 'host' => 'localhost', 'driver' => 'mssql'),
);
[/php]
The result of using
useAttributeAsKey
will be an array like this:[php]
array(
'primary' => array('host" => 'localhost', 'driver' => 'mysql'),
'secondary' => array('host' => 'localhost', 'driver' => 'mssql'),
);
[/php]
Good luck!
Hi Matthias,
Thank you very much for this very nice explanation of a Component that is unfortunately not yet good documented.
I have one question, what is the goal of a "prototype" ?
Thank you
Thank you!
A prototype can be used to add a definition which may be repeated many times inside the current node. According to the prototype definition in the example above, it is possible to have multiple connection arrays (containing a "driver", "host", etc. key), like:
[php]
array('connections' => array(
array('driver' => '...', 'host' => '...'),
array('driver' => '...', 'host' => '...'),
);
[/php]
Good luck!
Thanks for your clarification.
Regards,
Christophe