Le composant Config\Definition

Stof

array_merge, et bien plus...

Buts du composant Config\Definition

Merger plusieurs tableaux ensemble

Premier cas simple

Tableaux en entrée:

array(
    'foo' => 'bar',
    'bar' => array('some', 'awesome', 'thing'),
    'baz' => array('first' => true, 'second' => true),
)

array(
    'foo' => 'baz',
    'bar' => array('other', 'stuff'),
    'baz' => array('first' => false),
)
        

Résultat attendu:

array(
    'foo' => 'baz',
    'bar' => array('some', 'awesome', 'thing', 'other', 'stuff'),
    'baz' => array('first' => false, 'second' => true),
)
        

Functions simples: array_merge

$config = array_merge(
    array(
        'foo' => 'bar',
        'bar' => array('some', 'awesome', 'thing'),
        'baz' => array('first' => true, 'second' => true),
    ),
    array(
        'foo' => 'baz',
        'bar' => array('other', 'stuff'),
        'baz' => array('first' => false),
    )
);
    
array 'foo' => string 'baz' (length=3) 'bar' => array 0 => string 'other' (length=5) 1 => string 'stuff' (length=5) 'baz' => array 'first' => boolean false 'second' => boolean true

Remarque: array_replace fait la même chose pour un tableau associatif.

Functions simples: array_merge_recursive

Nos configurations sont souvent sur plusieurs niveaux.

Premier essai: array_merge_recursive

$config = array_merge_recursive(
    array(
        'foo' => 'bar',
        'bar' => array('some', 'thing'),
        'baz' => array('first' => true, 'second' => true),
    ),
    array(
        'foo' => 'baz',
        'bar' => array('other', 'awesome', 'stuff'),
        'baz' => array('first' => false),
    )
);
    

Functions simples: array_merge_recursive

array 'foo' => array 0 => string 'bar' (length=3) 1 => string 'baz' (length=3) 'bar' => array 0 => string 'some' (length=4) 1 => string 'awesome' (length=7) 2 => string 'thing' (length=5) 3 => string 'other' (length=5) 4 => string 'stuff' (length=5) 'baz' => array 'first' => array 0 => boolean true 1 => boolean false 'second' => boolean true

Echec dès qu'un tableau associatif entre en scène

Functions simples: array_replace_recursive

Nos configurations sont souvent sur plusieurs niveaux.

Deuxième essai: array_replace_recursive

$config = array_replace_recursive(
    array(
        'foo' => 'bar',
        'bar' => array('some', 'awesome', 'thing'),
        'baz' => array('first' => true, 'second' => true),
    ),
    array(
        'foo' => 'baz',
        'bar' => array('other', 'stuff'),
        'baz' => array('first' => false, 'second' => true),
    )
);
    

Functions simples: array_replace_recursive

array 'foo' => string 'baz' (length=3) 'bar' => array 0 => string 'other' (length=5) 1 => string 'stuff' (length=5) 2 => string 'thing' (length=5) 'baz' => array 'first' => boolean false 'second' => boolean true

Echec dès qu'un tableau non-associatif entre en scène

Les functions standards de PHP ne suffisent pas !

Symfony2 est là pour nous aider !

https://github.com/symfony/Config

La construction de l'arbre

$treeBuilder = new \Symfony\Component\Config\Definition\Builder\TreeBuilder();

$root = $tb->root('my_config');

$root
    ->children()
        ->scalarNode('foo')->end()
        ->arrayNode('bar')
            ->prototype('scalar')->end()
        ->end()
        ->arrayNode('baz')
            ->children()
                ->booleanNode('first')->end()
                ->booleanNode('second')->end()
            ->end()
        ->end()
    ->end();

    

L'utilisation de cet arbre sur nos données

$input = array(
    array(
        'foo' => 'bar',
        'bar' => array('some', 'awesome', 'thing'),
        'baz' => array('first' => true, 'second' => true),
    ),
    array(
        'foo' => 'baz',
        'bar' => array('other', 'stuff'),
        'baz' => array('first' => false),
    ),
);

$processor = new \Symfony\Component\Config\Definition\Processor();

$tree = $treeBuilder->buildTree();

$config = $processor->process($tree, $input);
    

La victoire avec Symfony

array 'foo' => string 'baz' (length=3) 'bar' => array 0 => string 'some' (length=4) 1 => string 'awesome' (length=7) 2 => string 'thing' (length=5) 3 => string 'other' (length=5) 4 => string 'stuff' (length=5) 'baz' => array 'first' => boolean false 'second' => boolean true

Voici notre configuration !

Nouveau défi: ajouter des valeurs par défaut

La structure de la configuration doit rester la même, mais on veut maintenant obtenir ce résultat quand aucune donnée n'est passée

$tb = new TreeBuilder();
$root = $tb->root('my_config');

$root
    ->children()
        ->scalarNode('foo')->defaultValue('baz')->end()
        ->arrayNode('bar')
            ->defaultValue(array('some', 'awesome', 'thing', 'other', 'stuff'))
            ->prototype('scalar')->end()
        ->end()
        ->arrayNode('baz')
            ->addDefaultsIfNotSet()
            ->children()
                ->booleanNode('first')->defaultFalse()->end()
                ->booleanNode('second')->defaultTrue()->end()
            ->end()
        ->end()
    ->end();
    

Remarque: Pour le noeud prototypé, la valeur par défault n'est appliquée que s'il n'est jamais ajouté.
Pour ajouter des éléments présents quelque soit l'entrée, il faut rajouter un tableau dans les entrées.

Nouveau défi: normaliser les données

Tableaux en entrée:

$input = array(
    array(
        'foo' => 'bar',
        'baz' => array('first' => true, 'second' => true, 'third' => array('foo' => true)),
    ),
    array(
        'foo' => 'baz',
        'baz' => array('first' => array(true, false), 'third' => false),
    ),
);
        

Résultat attendu:

array(
    'foo' => 'baz',
    'baz' => array(
        'first' => array(true, true, false),
        'second' => true,
        'third' => array('foo' => true, 'default' => false),
    ),
)
        

La construction de l'arbre

$treeBuilder = new \Symfony\Component\Config\Definition\Builder\TreeBuilder();

$root = $tb->root('my_config');

$root
    ->children()
        ->arrayNode('baz')
            ->children()
                ->arrayNode('first')
                    ->beforeNormalization()
                        ->ifTrue(function($v) {return is_scalar($v);})
                        ->then(function($v) {return array($v);})
                    ->end()
                    ->prototype('boolean')->end()
                ->end()
                ->booleanNode('second')->end()
                ->arrayNode('third')
                    ->useAttributeAsKey('key')
                    ->beforeNormalization()
                        ->ifTrue(function($v) {return is_scalar($v);})
                        ->then(function($v) {return array('default' => $v);})
                    ->end()
                    ->prototype('boolean')->end()
                ->end()
            ->end()
        ->end()
        ->scalarNode('foo')->end()
    ->end();
    

Et voilà le résultat

array 'baz' => array 'first' => array 0 => boolean true 1 => boolean true 2 => boolean false 'second' => boolean true 'third' => array 'foo' => boolean true 'default' => boolean false 'foo' => string 'baz' (length=3)

Valider les données

Règles disponibles de base

$root
    ->children()
        ->scalarNode('foo')->defaultValue('baz')->cannotBeEmpty()->end()
        ->scalarNode('can be defined only once')->cannotBeOverwritten()->isRequired()->end()
        ->arrayNode('bar')
            ->requiresAtLeastOneElement()
            ->defaultValue(array('some'))
            ->prototype('scalar')->end()
        ->end()
        ->arrayNode('baz')
            ->children()
                ->booleanNode('first')->isRequired()->end()
                ->booleanNode('second')->defaultTrue()->end()
            ->end()
        ->end()
    ->end();
    
$input = array(
    array('foo' => ''),
);
    
Symfony\Component\Config\Definition\Exception\InvalidConfigurationException The path "my_config.foo" cannot contain an empty value, but got "".

Valider les données

Règles disponibles de base

$root
    ->children()
        ->scalarNode('foo')->defaultValue('baz')->cannotBeEmpty()->end()
        ->scalarNode('can be defined only once')->cannotBeOverwritten()->isRequired()->end()
        ->arrayNode('bar')
            ->requiresAtLeastOneElement()
            ->defaultValue(array('some'))
            ->prototype('scalar')->end()
        ->end()
        ->arrayNode('baz')
            ->children()
                ->booleanNode('first')->isRequired()->end()
                ->booleanNode('second')->defaultTrue()->end()
            ->end()
        ->end()
    ->end();
    
$input = array(
);
    
Symfony\Component\Config\Definition\Exception\InvalidConfigurationException The child node "can be defined only once" at path "my_config" must be configured.

Valider les données

Règles disponibles de base

$root
    ->children()
        ->scalarNode('foo')->defaultValue('baz')->cannotBeEmpty()->end()
        ->scalarNode('can be defined only once')->cannotBeOverwritten()->isRequired()->end()
        ->arrayNode('bar')
            ->requiresAtLeastOneElement()
            ->defaultValue(array('some'))
            ->prototype('scalar')->end()
        ->end()
        ->arrayNode('baz')
            ->children()
                ->booleanNode('first')->isRequired()->end()
                ->booleanNode('second')->defaultTrue()->end()
            ->end()
        ->end()
    ->end();
    
$input = array(
    array('can be defined only once' => 3),
    array('can be defined only once' => 4),
);
    
Symfony\Component\Config\Definition\Exception\ForbiddenOverwriteException Configuration path "my_config.can be defined only once" cannot be overwritten. You have to define all options for this path, and any of its sub-paths in one configuration section.

Valider les données

Règles disponibles de base

$root
    ->children()
        ->scalarNode('foo')->defaultValue('baz')->cannotBeEmpty()->end()
        ->scalarNode('can be defined only once')->cannotBeOverwritten()->isRequired()->end()
        ->arrayNode('bar')
            ->requiresAtLeastOneElement()
            ->defaultValue(array('some'))
            ->prototype('scalar')->end()
        ->end()
        ->arrayNode('baz')
            ->children()
                ->booleanNode('first')->isRequired()->end()
                ->booleanNode('second')->defaultTrue()->end()
            ->end()
        ->end()
    ->end();
    
$input = array(
    array('can be defined only once' => 4),
    array('bar' => array()),
);
    
Symfony\Component\Config\Definition\Exception\InvalidConfigurationException The path "my_config.bar" should have at least 1 element(s) defined.

Valider les données

Règles disponibles de base

$root
    ->children()
        ->scalarNode('foo')->defaultValue('baz')->cannotBeEmpty()->end()
        ->scalarNode('can be defined only once')->cannotBeOverwritten()->isRequired()->end()
        ->arrayNode('bar')
            ->requiresAtLeastOneElement()
            ->defaultValue(array('some'))
            ->prototype('scalar')->end()
        ->end()
        ->arrayNode('baz')
            ->children()
                ->booleanNode('first')->isRequired()->end()
                ->booleanNode('second')->defaultTrue()->end()
            ->end()
        ->end()
    ->end();
    
$input = array(
    array('foo' => 'bar', 'can be defined only once' => 4),
    array('baz' => array('second' => false)),
);
    
Symfony\Component\Config\Definition\Exception\InvalidConfigurationException The child node "first" at path "my_config.baz" must be configured.

Valider les données

Règles disponibles de base

$root
    ->children()
        ->scalarNode('foo')->defaultValue('baz')->cannotBeEmpty()->end()
        ->scalarNode('can be defined only once')->cannotBeOverwritten()->isRequired()->end()
        ->arrayNode('bar')
            ->requiresAtLeastOneElement()
            ->defaultValue(array('some'))
            ->prototype('scalar')->end()
        ->end()
        ->arrayNode('baz')
            ->children()
                ->booleanNode('first')->isRequired()->end()
                ->booleanNode('second')->defaultTrue()->end()
            ->end()
        ->end()
    ->end();
    
$input = array(
    array('can be defined only once' => 4),
    array('baz' => array('first' => false)),
);
    

Configuration acceptée

Valider les données

Règles personnalisées

$root
    ->children()
        ->scalarNode('foo')
            ->defaultValue('pouet')
            ->validate()
                ->ifNotInArray(array('foo', 'bar', 'baz'))
                ->thenInvalid('The value %s is not allowed')
            ->end()
            ->validate()
                ->ifTrue(function($v) {return 'baz' === $v;})
                ->then(function($v) {return 'hello ' . $v;})
            ->end()
        ->end()
    ->end();
    

Souvenir, souvenir

Comportement d'array_merge_recursive

$config = array_merge_recursive(
    array(
        'baz' => array('first' => true, 'second' => true),
    ),
    array(
        'baz' => array('first' => false),
    )
);
    
array 'baz' => array 'first' => array 0 => boolean true 1 => boolean false 'second' => boolean true

Souvenir, souvenir

Comportement d'array_merge_recursive

function getNode($name) {
    $tb = new TreeBuilder();

    return $tb->root($name)
        ->beforeNormalization()
            ->ifTrue(function($v) {return is_scalar($v);})
            ->then(function($v) {return array($v);})
        ->end()
        ->prototype('boolean')->end()
        ->validate()
            ->ifTrue(function($v) {return 1 === count($v);})
            ->then(function($v) {return current($v);})
        ->end();
}

$root
    ->children()
        ->arrayNode('baz')
            ->append(getNode('first'))
            ->append(getNode('second'))
        ->end()
    ->end();
    

Souvenir, souvenir

Comportement d'array_merge_recursive

array 'baz' => array 'first' => array 0 => boolean true 1 => boolean false 'second' => boolean true

Merci

Questions ?