diff --git a/src/Symfony/Component/DependencyInjection/Configuration/ArrayNode.php b/src/Symfony/Component/DependencyInjection/Configuration/ArrayNode.php new file mode 100644 index 000000000000..936c16fcac21 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Configuration/ArrayNode.php @@ -0,0 +1,105 @@ +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; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Configuration/BaseNode.php b/src/Symfony/Component/DependencyInjection/Configuration/BaseNode.php new file mode 100644 index 000000000000..69f5847fbcd7 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Configuration/BaseNode.php @@ -0,0 +1,64 @@ +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); +} \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Configuration/Builder/ExprBuilder.php b/src/Symfony/Component/DependencyInjection/Configuration/Builder/ExprBuilder.php new file mode 100644 index 000000000000..c8f2e0cbdd84 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Configuration/Builder/ExprBuilder.php @@ -0,0 +1,62 @@ +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; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Configuration/Builder/NodeBuilder.php b/src/Symfony/Component/DependencyInjection/Configuration/Builder/NodeBuilder.php new file mode 100644 index 000000000000..0c63068d7143 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Configuration/Builder/NodeBuilder.php @@ -0,0 +1,92 @@ +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; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Configuration/Builder/TreeBuilder.php b/src/Symfony/Component/DependencyInjection/Configuration/Builder/TreeBuilder.php new file mode 100644 index 000000000000..93f48bdf1936 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Configuration/Builder/TreeBuilder.php @@ -0,0 +1,90 @@ +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; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Configuration/Exception/Exception.php b/src/Symfony/Component/DependencyInjection/Configuration/Exception/Exception.php new file mode 100644 index 000000000000..e5a464b27694 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Configuration/Exception/Exception.php @@ -0,0 +1,7 @@ + + */ +interface PrototypeNodeInterface extends NodeInterface +{ + function setName($name); +} \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Configuration/ScalarNode.php b/src/Symfony/Component/DependencyInjection/Configuration/ScalarNode.php new file mode 100644 index 000000000000..cdcebe3dac6a --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Configuration/ScalarNode.php @@ -0,0 +1,29 @@ +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; + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/DependencyInjection/Configuration/NormalizationTest.php b/tests/Symfony/Tests/Component/DependencyInjection/Configuration/NormalizationTest.php new file mode 100644 index 000000000000..4afbdb9eeeb8 --- /dev/null +++ b/tests/Symfony/Tests/Component/DependencyInjection/Configuration/NormalizationTest.php @@ -0,0 +1,127 @@ +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)); + } +} \ No newline at end of file