Después de cargar los valores de configuración de todo tipo de recursos, los valores y su estructura se pueden validar usando la parte «Definición» del componente Config. Generalmente, se espera que los valores de configuración muestren algún tipo de jerarquía. Además, los valores deben ser de un determinado tipo, estar limitados en número o ser uno de un determinado conjunto de valores. Por ejemplo, la siguiente configuración (en YAML) muestra una clara jerarquía y algunas reglas de validación que se deben aplicar (como: «el valor de auto_connect debe ser un valor booleano»):
auto_connect: true
default_connection: mysql
connections:
mysql:
host: localhost
driver: mysql
username: user
password: pass
sqlite:
host: localhost
driver: sqlite
memory: true
username: user
password: pass
Al cargar varios archivos de configuración, debería ser posible combinar y sobrescribir algunos valores. Otros valores no se deben fusionar y quedarse como estaban cuando se encontraron por primera vez. Además, algunas claves sólo están disponibles cuando otra clave tiene un valor específico (en la configuración del ejemplo anterior: la clave memory sólo tiene sentido cuando driver es sqlite).
Puedes definir todas las normas relativas a los valores de configuración usando el Symfony\Component\Config\Definition\Builder\TreeBuilder.
Debes devolver una instancia de Symfony\Component\Config\Definition\Builder\TreeBuilder desde una clase Configuration personalizada que implemente la Symfony\Component\Config\Definition\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');
// ... añade definiciones de nodo a la raíz del árbol
return $treeBuilder;
}
}
Un árbol contiene definiciones de nodo que se pueden establecer de manera semántica. Esto significa que, usando la sangría y una sencilla notación, es posible reflejar la estructura real de los valores de configuración:
$rootNode
->children()
->booleanNode('auto_connect')
->defaultTrue()
->end()
->scalarNode('default_connection')
->defaultValue('default')
->end()
->end()
;
El nodo raíz en sí es un arreglo de nodos, y tiene hijos, como el nodo booleano auto_connect y el nodo escalar default_connection. En general: después de definir un nodo, una llamada a end() te lleva un paso más arriba en la jerarquía.
Es posible validar el tipo de un valor proporcionado utilizando la definición de nodo apropiada. El tipo de nodo está disponible para:
y se crean con node($nombre, $tipo) o su método abreviado asociado xxxxNode($nombre).
Nuevo en la versión 2.2: Los nodos numéricos (float e integer) son nuevos en 2.2
Los nodos numéricos (float e integer) proporcionan dos restricciones extra — min() y max() — que te permiten validar el valor:
$rootNode
->children()
->integerNode('positive_value')
->min(0)
->end()
->floatNode('big_value')
->max(5E45)
->end()
->integerNode('value_inside_a_range')
->min(-50)->max(50)
->end()
->end()
;
Es posible añadir un nivel más profundo a la jerarquía, añadiendo un nodo al arreglo. El arreglo de nodos en sí mismo, puede tener un conjunto predefinido de variables de nodo:
$rootNode
->children()
->arrayNode('connection')
->children()
->scalarNode('driver')->end()
->scalarNode('host')->end()
->scalarNode('username')->end()
->scalarNode('password')->end()
->end()
->end()
->end()
;
O puedes definir un prototipo para cada nodo dentro del arreglo de nodos:
$rootNode
->children()
->arrayNode('connections')
->prototype('array')
->children()
->scalarNode('driver')->end()
->scalarNode('host')->end()
->scalarNode('username')->end()
->scalarNode('password')->end()
->end()
->end()
->end()
;
Puedes utilizar un prototipo para añadir una definición la cual se puede repetir muchas veces dentro del nodo actual. De acuerdo con la definición del prototipo en el ejemplo anterior, es posible tener varios arreglos de conexión (conteniendo un driver, host, etc.).
Antes de definir los hijos de un arreglo de nodos, puedes proveer opciones como:
Un ejemplo de esto:
$rootNode
->children()
->arrayNode('parameters')
->isRequired()
->requiresAtLeastOneElement()
->useAttributeAsKey('name')
->prototype('array')
->children()
->scalarNode('value')->isRequired()->end()
->end()
->end()
->end()
->end()
;
En YAML, la configuración podría tener esta apariencia:
database:
parameters:
param1: { value: param1val }
En XML, cada nodo parameters tendría un atributo name (junto con value), el cual se sacaría y utilizarí como la clave para ese elemento en el arreglo final. El useAttributeAsKey es útil para normalizar cómo se especifican los arreglos entre diferentes formatos como XML y YAML.
Para todos los tipos de nodo, es posible definir valores predeterminados y valores de sustitución en caso de que un nodo tenga un cierto valor:
$rootNode
->children()
->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()
->arrayNode('settings')
->addDefaultsIfNotSet()
->children()
->scalarNode('name')
->isRequired()
->cannotBeEmpty()
->defaultValue('value')
->end()
->end()
->end()
->end()
;
Nuevo en la versión 2.2: Los métodos canBeEnabled y canBeDisabled son nuevos en Symfony 2.2
Si tienes secciones enteras que son opcionales y se pueden activar/desactivar, puedes aprovechar los métodos abreviados canBeEnabled() y canBeDisabled():
$arrayNode
->canBeEnabled()
;
// es equivalente a
$arrayNode
->treatFalseLike(array('enabled' => false))
->treatTrueLike(array('enabled' => true))
->treatNullLike(array('enabled' => true))
->children()
->booleanNode('enabled')
->defaultFalse()
;
El método canBeDisabled se ve igual salvo que de manera predefinida la sección estaría habilitada.
Puedes proporcionar opciones adicionales sobre el proceso de mezcla. Para arreglos:
Para todos los nodos:
Si tienes una configuración de validación compleja, entonces el árbol puede crecer bastante y posiblemente quieras dividirlo en secciones. Lo puedes conseguir haciendo una sección en un nodo separado y luego anexándolo al árbol principal con append():
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('database');
$rootNode
->children()
->arrayNode('connection')
->children()
->scalarNode('driver')
->isRequired()
->cannotBeEmpty()
->end()
->scalarNode('host')
->defaultValue('localhost')
->end()
->scalarNode('username')->end()
->scalarNode('password')->end()
->booleanNode('memory')
->defaultFalse()
->end()
->end()
->append($this->addParametersNode())
->end()
->end()
;
return $treeBuilder;
}
public function addParametersNode()
{
$builder = new TreeBuilder();
$node = $builder->root('parameters');
$node
->isRequired()
->requiresAtLeastOneElement()
->useAttributeAsKey('name')
->prototype('array')
->children()
->scalarNode('value')->isRequired()->end()
->end()
->end()
;
return $node;
}
Esto también es útil para ayudarte a evitar repeticiones si tienes repetida la configuración de esas secciones en diferentes sitios.
Cuándo son procesados los archivos de configuración primero se normalizan, luego se fusionan y finalmente el árbol se usa para validar el arreglo resultante. El proceso de normalización se usa para sacar algunos de las diferencias que resultan de diferentes formatos de configuración, principalmente las diferencias entre Yaml y XML.
El separador utilizado en las claves en Yaml típicamente es _ y - en XML. Por ejemplo, auto_connect en Yaml y auto-connect en XML. La normalización debería hacer que ambos fueran auto_connect.
Prudencia
La clave destino no será alterada si está mezclada como foo-bar_moo o si ya existe.
Otra diferencia entre Yaml y XML es la manera en que se pueden representar los valores del arreglo. En Yaml puedes tener:
twig:
extensions: ['twig.extension.foo', 'twig.extension.bar']
y en XML:
<twig:config>
<twig:extension>twig.extension.foo</twig:extension>
<twig:extension>twig.extension.bar</twig:extension>
</twig:config>
Esta diferencia se puede sacar en la normalización pluralizando la clave utilizada en XML. Puedes especificar si deseas que un clave se pluralice de este modo con fixXmlConfig():
$rootNode
->fixXmlConfig('extension')
->children()
->arrayNode('extensions')
->prototype('scalar')->end()
->end()
->end()
;
Si es una pluralización irregular puedes especificar el plural a utilizar como segundo argumento:
$rootNode
->fixXmlConfig('child', 'children')
->children()
->arrayNode('children')
->end()
;
Así como se corrigió este, fixXmlConfig garantiza que solo los elementos XML todavía se convierten en un arreglo. Por lo tanto puedes tener:
<connection>default</connection>
<connection>extra</connection>
y a veces sólo:
<connection>default</connection>
De manera predefinida connection sería un arreglo en el primer caso y una cadena en el segundo dificultando su validación. Puedes garantizar que siempre sea un arreglo con con fixXmlConfig.
Puedes controlar el proceso de normalización más allá, de ser necesario. Por ejemplo, posiblemente quieras permitir que se pueda configurar una cadena y utilizarla como una clave particular o que se configuren varias claves explícitamente. De modo que, si todo aparte del name es opcional en esta configuración:
connection:
name: my_mysql_connection
host: localhost
driver: mysql
username: user
password: pass
También puedes permitir lo siguiente:
connection: my_mysql_connection
Cambiando el valor de una cadena a un arreglo asociativo con name como la clave:
$rootNode
->children()
->arrayNode('connection')
->beforeNormalization()
->ifString()
->then(function($v) { return array('name'=> $v); })
->end()
->children()
->scalarNode('name')->isRequired()
// ...
->end()
->end()
->end()
;
Puedes proporcionar reglas de validación más avanzadas utilizando la Symfony\Component\Config\Definition\Builder\ExprBuilder. Este constructor implementa una interfaz fluida para un bien conocido control de la estructura. El constructor se utiliza para agregar reglas avanzadas de validación a las definiciones de nodo, como:
$rootNode
->children()
->arrayNode('connection')
->children()
->scalarNode('driver')
->isRequired()
->validate()
->ifNotInArray(array('mysql', 'sqlite', 'mssql'))
->thenInvalid('Invalid database driver "%s"')
->end()
->end()
->end()
->end()
->end()
;
Una regla de validación siempre tiene una parte «if». Puedes especificar esta parte de las siguientes maneras:
Una regla de validación también requiere una parte «then»:
Normalmente, «then» es un cierre. Su valor de retorno se utiliza como un nuevo valor para el nodo, en lugar del valor original.
La clase Symfony\Component\Config\Definition\Processor utiliza el árbol que fue construido usando el Symfony\Component\Config\Definition\Builder\TreeBuilder para procesar múltiples arreglos de valores de configuración que se deben combinar. Si algún valor no es del tipo esperado, todavía es obligatorio e indefinido, o no se pudo validar de alguna otra manera, será lanzada una excepción. De lo contrario, el resultado es un arreglo de valores de configuración limpio:
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)
;