[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:
parent
a28151a8af
commit
b484763a7a
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\DependencyInjection\Configuration\Exception;
|
||||
|
||||
class Exception extends \RuntimeException
|
||||
{
|
||||
}
|
@ -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
|
||||
{
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\DependencyInjection\Configuration;
|
||||
|
||||
interface NodeInterface
|
||||
{
|
||||
function getName();
|
||||
function getPath();
|
||||
function normalize($value);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user