[DependencyInjection] added first version of the config normalizer

This is mainly intended for complex configurations to ease the work you
have with normalizing different configuration formats (YAML, XML, and PHP).

First, you have to set-up a config tree:

    $treeBuilder = new TreeBuilder();
    $tree = $treeBuilder
        ->root('security_config', 'array')
            ->node('access_denied_url', 'scalar')->end()
            ->normalize('encoder')
            ->node('encoders', 'array')
                ->key('class')
                ->prototype('array')
                    ->before()->ifString()->then(function($v) { return array('algorithm' => $v); })->end()
                    ->node('algorithm', 'scalar')->end()
                    ->node('encode_as_base64', 'scalar')->end()
                    ->node('iterations', 'scalar')->end()
                ->end()
            ->end()
        ->end()
        ->buildTree()
    ;

This tree and the metadata attached to the different nodes is then used
to intelligently transform the passed config array:

    $normalizedConfig = $tree->normalize($config);
This commit is contained in:
Johannes Schmitt 2011-01-30 15:18:49 +01:00 committed by Fabien Potencier
parent a28151a8af
commit b484763a7a
11 changed files with 611 additions and 0 deletions

View File

@ -0,0 +1,105 @@
<?php
namespace Symfony\Component\DependencyInjection\Configuration;
use Symfony\Component\DependencyInjection\Extension\Extension;
class ArrayNode extends BaseNode implements PrototypeNodeInterface
{
protected $normalizeTransformations;
protected $children;
protected $prototype;
protected $keyAttribute;
public function __construct($name, NodeInterface $parent = null, array $beforeTransformations = array(), array $afterTransformations = array(), array $normalizeTransformations = array(), $keyAttribute = null)
{
parent::__construct($name, $parent, $beforeTransformations, $afterTransformations);
$this->children = array();
$this->normalizeTransformations = $normalizeTransformations;
$this->keyAttribute = $keyAttribute;
}
public function setName($name)
{
$this->name = $name;
}
public function setPrototype(PrototypeNodeInterface $node)
{
if (count($this->children) > 0) {
throw new \RuntimeException('An ARRAY node must either have concrete children, or a prototype node.');
}
$this->prototype = $node;
}
public function addChild(NodeInterface $node)
{
$name = $node->getName();
if (empty($name)) {
throw new \InvalidArgumentException('Node name cannot be empty.');
}
if (isset($this->children[$name])) {
throw new \InvalidArgumentException(sprintf('The node "%s" already exists.', $name));
}
if (null !== $this->prototype) {
throw new \RuntimeException('An ARRAY node must either have a prototype, or concrete children.');
}
$this->children[$name] = $node;
}
protected function validateType($value)
{
if (!is_array($value)) {
throw new InvalidTypeException(sprintf(
'Invalid type for path "%s". Expected array, but got %s',
$this->getPath(),
json_encode($value)
));
}
}
protected function normalizeValue($value)
{
foreach ($this->normalizeTransformations as $transformation) {
list($singular, $plural) = $transformation;
if (!isset($value[$singular])) {
continue;
}
$value[$plural] = Extension::normalizeConfig($value, $singular, $plural);
}
if (null !== $this->prototype) {
$normalized = array();
foreach ($value as $k => $v) {
if (null !== $this->keyAttribute && is_array($v) && isset($v[$this->keyAttribute])) {
$k = $v[$this->keyAttribute];
}
$this->prototype->setName($k);
if (null !== $this->keyAttribute) {
$normalized[$k] = $this->prototype->normalize($v);
} else {
$normalized[] = $this->prototype->normalize($v);
}
}
return $normalized;
}
$normalized = array();
foreach ($this->children as $name => $child) {
if (!array_key_exists($name, $value)) {
continue;
}
$normalized[$name] = $child->normalize($value[$name]);
}
return $normalized;
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Symfony\Component\DependencyInjection\Configuration;
abstract class BaseNode implements NodeInterface
{
protected $name;
protected $parent;
protected $beforeTransformations;
protected $afterTransformations;
protected $nodeFactory;
public function __construct($name, NodeInterface $parent = null, $beforeTransformations = array(), $afterTransformations = array())
{
if (false !== strpos($name, '.')) {
throw new \InvalidArgumentException('The name must not contain ".".');
}
$this->name = $name;
$this->parent = $parent;
$this->beforeTransformations = $beforeTransformations;
$this->afterTransformations = $afterTransformations;
}
public function getName()
{
return $this->name;
}
public function getPath()
{
$path = $this->name;
if (null !== $this->parent) {
$path = $this->parent->getPath().'.'.$path;
}
return $path;
}
public final function normalize($value)
{
// run before transformations
foreach ($this->beforeTransformations as $transformation) {
$value = $transformation($value);
}
// validate type
$this->validateType($value);
// normalize value
$value = $this->normalizeValue($value);
// run after transformations
foreach ($this->afterTransformations as $transformation) {
$value = $transformation($value);
}
return $value;
}
abstract protected function validateType($value);
abstract protected function normalizeValue($value);
}

View File

@ -0,0 +1,62 @@
<?php
namespace Symfony\Component\DependencyInjection\Configuration\Builder;
class ExprBuilder
{
public $parent;
public $ifPart;
public $thenPart;
public function __construct($parent)
{
$this->parent = $parent;
}
public function ifTrue(\Closure $closure)
{
$this->ifPart = $closure;
return $this;
}
public function ifString()
{
$this->ifPart = function($v) { return is_string($v); };
return $this;
}
public function ifNull()
{
$this->ifPart = function($v) { return null === $v; };
return $this;
}
public function ifArray()
{
$this->ifPart = function($v) { return is_array($v); };
return $this;
}
public function then(\Closure $closure)
{
$this->thenPart = $closure;
return $this;
}
public function end()
{
if (null === $this->ifPart) {
throw new \RuntimeException('You must specify an if part.');
}
if (null === $this->thenPart) {
throw new \RuntimeException('You must specify a then part.');
}
return $this->parent;
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace Symfony\Component\DependencyInjection\Configuration\Builder;
class NodeBuilder
{
/************
* READ-ONLY
************/
public $name;
public $type;
public $key;
public $parent;
public $children;
public $prototype;
public $normalizeTransformations;
public $beforeTransformations;
public $afterTransformations;
public function __construct($name, $type, $parent = null)
{
$this->name = $name;
$this->type = $type;
$this->parent = $parent;
$this->children =
$this->beforeTransformations =
$this->afterTransformations =
$this->normalizeTransformations = array();
}
/****************************
* FLUID INTERFACE
****************************/
public function node($name, $type)
{
$node = new NodeBuilder($name, $type, $this);
return $this->children[$name] = $node;
}
public function normalize($key, $plural = null)
{
if (null === $plural) {
$plural = $key.'s';
}
$this->normalizeTransformations[] = array($key, $plural);
return $this;
}
public function key($name)
{
$this->key = $name;
return $this;
}
public function before(\Closure $closure = null)
{
if (null !== $closure) {
$this->beforeTransformations[] = $closure;
return $this;
}
return $this->beforeTransformations[] = new ExprBuilder($this);
}
public function prototype($type)
{
return $this->prototype = new NodeBuilder(null, $type, $this);
}
public function after(\Closure $closure = null)
{
if (null !== $closure) {
$this->afterTransformations[] = $closure;
return $this;
}
return $this->afterTransformations[] = new ExprBuilder($this);
}
public function end()
{
return $this->parent;
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace Symfony\Component\DependencyInjection\Configuration\Builder;
use Symfony\Component\DependencyInjection\Configuration\ArrayNode;
use Symfony\Component\DependencyInjection\Configuration\ScalarNode;
class TreeBuilder
{
protected $root;
protected $tree;
public function root($name, $type)
{
$this->tree = null;
return $this->root = new NodeBuilder($name, $type, $this);
}
public function buildTree()
{
if (null === $this->root) {
throw new \RuntimeException('You haven\'t added a root node.');
}
if (null !== $this->tree) {
return $this->tree;
}
$this->root->parent = null;
return $this->tree = $this->createConfigNode($this->root);
}
protected function createConfigNode(NodeBuilder $node)
{
$node->beforeTransformations = $this->buildExpressions($node->beforeTransformations);
$node->afterTransformations = $this->buildExpressions($node->afterTransformations);
$method = 'create'.$node->type.'ConfigNode';
if (!method_exists($this, $method)) {
throw new \RuntimeException(sprintf('Unknown node type: "%s"', $node->type));
}
return $this->$method($node);
}
protected function createScalarConfigNode(NodeBuilder $node)
{
return new ScalarNode($node->name, $node->parent, $node->beforeTransformations, $node->afterTransformations);
}
protected function createArrayConfigNode(NodeBuilder $node)
{
$configNode = new ArrayNode($node->name, $node->parent, $node->beforeTransformations, $node->afterTransformations, $node->normalizeTransformations, $node->key);
foreach ($node->children as $child) {
$child->parent = $configNode;
$configNode->addChild($this->createConfigNode($child));
}
if (null !== $node->prototype) {
$node->prototype->parent = $configNode;
$configNode->setPrototype($this->createConfigNode($node->prototype));
}
return $configNode;
}
protected function buildExpressions(array $expressions)
{
foreach ($expressions as $k => $expr) {
if (!$expr instanceof ExprBuilder) {
continue;
}
$expressions[$k] = function($v) use($expr) {
$ifPart = $expr->ifPart;
if (true !== $ifPart($v)) {
return $v;
}
$thenPart = $expr->thenPart;
return $thenPart($v);
};
}
return $expressions;
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Symfony\Component\DependencyInjection\Configuration\Exception;
class Exception extends \RuntimeException
{
}

View File

@ -0,0 +1,12 @@
<?php
namespace Symfony\Component\DependencyInjection\Configuration\Exception;
/**
* This exception is thrown if an invalid type is encountered.
*
* @author johannes
*/
class InvalidTypeException extends Exception
{
}

View File

@ -0,0 +1,10 @@
<?php
namespace Symfony\Component\DependencyInjection\Configuration;
interface NodeInterface
{
function getName();
function getPath();
function normalize($value);
}

View File

@ -0,0 +1,13 @@
<?php
namespace Symfony\Component\DependencyInjection\Configuration;
/**
* This interface must be implemented by nodes which can be used as prototypes.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
interface PrototypeNodeInterface extends NodeInterface
{
function setName($name);
}

View File

@ -0,0 +1,29 @@
<?php
namespace Symfony\Component\DependencyInjection\Configuration;
use Symfony\Component\DependencyInjection\Configuration\Exception\InvalidTypeException;
class ScalarNode extends BaseNode implements PrototypeNodeInterface
{
public function setName($name)
{
$this->name = $name;
}
protected function validateType($value)
{
if (!is_scalar($value)) {
throw new \InvalidTypeException(sprintf(
'Invalid type for path "%s". Expected scalar, but got %s.',
$this->getPath(),
json_encode($value)
));
}
}
protected function normalizeValue($value)
{
return $value;
}
}

View File

@ -0,0 +1,127 @@
<?php
namespace Symfony\Tests\Component\DependencyInjection\Configuration;
use Symfony\Component\DependencyInjection\Configuration\NodeInterface;
use Symfony\Component\DependencyInjection\Configuration\Builder\TreeBuilder;
class NormalizerTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider getEncoderTests
*/
public function testNormalizeEncoders($denormalized)
{
$tb = new TreeBuilder();
$tree = $tb
->root('root_name', 'array')
->normalize('encoder')
->node('encoders', 'array')
->key('class')
->prototype('array')
->before()->ifString()->then(function($v) { return array('algorithm' => $v); })->end()
->node('algorithm', 'scalar')->end()
->end()
->end()
->end()
->buildTree()
;
$normalized = array(
'encoders' => array(
'foo' => array('algorithm' => 'plaintext'),
),
);
$this->assertNormalized($tree, $denormalized, $normalized);
}
public function getEncoderTests()
{
$configs = array();
// XML
$configs[] = array(
'encoder' => array(
array('class' => 'foo', 'algorithm' => 'plaintext'),
),
);
// XML when only one element of this type
$configs[] = array(
'encoder' => array('class' => 'foo', 'algorithm' => 'plaintext'),
);
// YAML/PHP
$configs[] = array(
'encoders' => array(
array('class' => 'foo', 'algorithm' => 'plaintext'),
),
);
// YAML/PHP
$configs[] = array(
'encoders' => array(
'foo' => 'plaintext',
),
);
// YAML/PHP
$configs[] = array(
'encoders' => array(
'foo' => array('algorithm' => 'plaintext'),
),
);
return array_map(function($v) {
return array($v);
}, $configs);
}
/**
* @dataProvider getAnonymousKeysTests
*/
public function testAnonymousKeysArray($denormalized)
{
$tb = new TreeBuilder();
$tree = $tb
->root('root', 'array')
->node('logout', 'array')
->normalize('handler')
->node('handlers', 'array')
->prototype('scalar')->end()
->end()
->end()
->end()
->buildTree()
;
$normalized = array('logout' => array('handlers' => array('a', 'b', 'c')));
$this->assertNormalized($tree, $denormalized, $normalized);
}
public function getAnonymousKeysTests()
{
$configs = array();
$configs[] = array(
'logout' => array(
'handlers' => array('a', 'b', 'c'),
),
);
$configs[] = array(
'logout' => array(
'handler' => array('a', 'b', 'c'),
),
);
return array_map(function($v) { return array($v); }, $configs);
}
public static function assertNormalized(NodeInterface $tree, $denormalized, $normalized)
{
self::assertSame($normalized, $tree->normalize($denormalized));
}
}