diff --git a/src/Symfony/Component/Config/CHANGELOG.md b/src/Symfony/Component/Config/CHANGELOG.md
index 8426abd57b..63c244078f 100644
--- a/src/Symfony/Component/Config/CHANGELOG.md
+++ b/src/Symfony/Component/Config/CHANGELOG.md
@@ -6,6 +6,7 @@ CHANGELOG
* added numerical type handling for config definitions
* added convenience methods for optional configuration sections to ArrayNodeDefinition
+ * added a utils class for XML manipulations
2.1.0
-----
diff --git a/src/Symfony/Component/Config/Tests/Fixtures/Util/document_type.xml b/src/Symfony/Component/Config/Tests/Fixtures/Util/document_type.xml
new file mode 100644
index 0000000000..4c25228263
--- /dev/null
+++ b/src/Symfony/Component/Config/Tests/Fixtures/Util/document_type.xml
@@ -0,0 +1,3 @@
+
+]>
+
diff --git a/src/Symfony/Component/Config/Tests/Fixtures/Util/invalid.xml b/src/Symfony/Component/Config/Tests/Fixtures/Util/invalid.xml
new file mode 100644
index 0000000000..a07af9fd85
--- /dev/null
+++ b/src/Symfony/Component/Config/Tests/Fixtures/Util/invalid.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/Symfony/Component/Config/Tests/Fixtures/Util/invalid_schema.xml b/src/Symfony/Component/Config/Tests/Fixtures/Util/invalid_schema.xml
new file mode 100644
index 0000000000..e2725a2c2a
--- /dev/null
+++ b/src/Symfony/Component/Config/Tests/Fixtures/Util/invalid_schema.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/Symfony/Component/Config/Tests/Fixtures/Util/schema.xsd b/src/Symfony/Component/Config/Tests/Fixtures/Util/schema.xsd
new file mode 100644
index 0000000000..e56820f691
--- /dev/null
+++ b/src/Symfony/Component/Config/Tests/Fixtures/Util/schema.xsd
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/src/Symfony/Component/Config/Tests/Fixtures/Util/valid.xml b/src/Symfony/Component/Config/Tests/Fixtures/Util/valid.xml
new file mode 100644
index 0000000000..a96bb38267
--- /dev/null
+++ b/src/Symfony/Component/Config/Tests/Fixtures/Util/valid.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php b/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php
new file mode 100644
index 0000000000..0ad0b19576
--- /dev/null
+++ b/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php
@@ -0,0 +1,124 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Config\Tests\Loader;
+
+use Symfony\Component\Config\Util\XmlUtils;
+
+class XmlUtilsTest extends \PHPUnit_Framework_TestCase
+{
+ public function testLoadFile()
+ {
+ $fixtures = __DIR__.'/../Fixtures/Util/';
+
+ try {
+ XmlUtils::loadFile($fixtures.'invalid.xml');
+ $this->fail();
+ } catch (\InvalidArgumentException $e) {
+ $this->assertContains('ERROR 77', $e->getMessage());
+ }
+
+ try {
+ XmlUtils::loadFile($fixtures.'document_type.xml');
+ $this->fail();
+ } catch (\InvalidArgumentException $e) {
+ $this->assertContains('Document types are not allowed', $e->getMessage());
+ }
+
+ try {
+ XmlUtils::loadFile($fixtures.'invalid_schema.xml', $fixtures.'schema.xsd');
+ $this->fail();
+ } catch (\InvalidArgumentException $e) {
+ $this->assertContains('ERROR 1845', $e->getMessage());
+ }
+
+ try {
+ XmlUtils::loadFile($fixtures.'invalid_schema.xml', 'invalid_callback_or_file');
+ $this->fail();
+ } catch (\InvalidArgumentException $e) {
+ $this->assertContains('XSD file or callable', $e->getMessage());
+ }
+
+ $mock = $this->getMock(__NAMESPACE__.'\Validator');
+ $mock->expects($this->exactly(2))->method('validate')->will($this->onConsecutiveCalls(false, true));
+
+ try {
+ XmlUtils::loadFile($fixtures.'valid.xml', array($mock, 'validate'));
+ $this->fail();
+ } catch (\InvalidArgumentException $e) {
+ $this->assertContains('is not valid', $e->getMessage());
+ }
+
+ $this->assertInstanceOf('DOMDocument', XmlUtils::loadFile($fixtures.'valid.xml', array($mock, 'validate')));
+ }
+
+ /**
+ * @dataProvider getDataForConvertDomToArray
+ */
+ public function testConvertDomToArray($expected, $xml)
+ {
+ $dom = new \DOMDocument();
+ $dom->loadXML(''.$xml.'');
+
+ $this->assertSame($expected, XmlUtils::convertDomElementToArray($dom->documentElement));
+ }
+
+ public function getDataForConvertDomToArray()
+ {
+ return array(
+ array(null, ''),
+ array(array('foo' => null), ''),
+ array(array('foo' => 'bar'), 'bar'),
+ array(array('foo' => array('foo' => 'bar')), ''),
+ array(array('foo' => array('foo' => 'bar')), 'bar'),
+ array(array('foo' => array('foo' => 'bar', 'value' => 'text')), 'text'),
+ array(array('foo' => array('attr' => 'bar', 'foo' => 'text')), 'text'),
+ array(array('foo' => array('bar', 'text')), 'bartext'),
+ array(array('foo' => array(array('foo' => 'bar'), array('foo' => 'text'))), ''),
+ array(array('foo' => array('foo' => array('bar', 'text'))), 'text'),
+ array(array('foo' => 'bar'), 'bar'),
+ );
+ }
+
+ /**
+ * @dataProvider getDataForPhpize
+ */
+ public function testPhpize($expected, $value)
+ {
+ $this->assertSame($expected, XmlUtils::phpize($value));
+ }
+
+ public function getDataForPhpize()
+ {
+ return array(
+ array(null, 'null'),
+ array(true, 'true'),
+ array(false, 'false'),
+ array(null, 'Null'),
+ array(true, 'True'),
+ array(false, 'False'),
+ array(0, '0'),
+ array(1, '1'),
+ array(0777, '0777'),
+ array(255, '0xFF'),
+ array(100.0, '1e2'),
+ array(-120.0, '-1.2E2'),
+ array(-10100.1, '-10100.1'),
+ array(-10100.1, '-10,100.1'),
+ array('foo', 'foo'),
+ );
+ }
+}
+
+interface Validator
+{
+ public function validate();
+}
diff --git a/src/Symfony/Component/Config/Util/XmlUtils.php b/src/Symfony/Component/Config/Util/XmlUtils.php
new file mode 100644
index 0000000000..8ab8225df5
--- /dev/null
+++ b/src/Symfony/Component/Config/Util/XmlUtils.php
@@ -0,0 +1,215 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Config\Util;
+
+/**
+ * XMLUtils is a bunch of utility methods to XML operations.
+ *
+ * This class contains static methods only and is not meant to be instantiated.
+ *
+ * @author Fabien Potencier
+ * @author Martin HasoĊ
+ */
+class XmlUtils
+{
+ /**
+ * This class should not be instantiated
+ */
+ private function __construct()
+ {
+ }
+
+ /**
+ * Loads an XML file.
+ *
+ * @param string $file An XML file path
+ * @param string|callable $schemaOrCallable An XSD schema file path or callable
+ *
+ * @return \DOMDocument
+ *
+ * @throws \InvalidArgumentException When loading of XML file returns error
+ */
+ public static function loadFile($file, $schemaOrCallable = null)
+ {
+ $internalErrors = libxml_use_internal_errors(true);
+ $disableEntities = libxml_disable_entity_loader(true);
+ libxml_clear_errors();
+
+ $dom = new \DOMDocument();
+ $dom->validateOnParse = true;
+ if (!$dom->loadXML(file_get_contents($file), LIBXML_NONET | (defined('LIBXML_COMPACT') ? LIBXML_COMPACT : 0))) {
+ libxml_disable_entity_loader($disableEntities);
+
+ throw new \InvalidArgumentException(implode("\n", static::getXmlErrors($internalErrors)));
+ }
+
+ $dom->normalizeDocument();
+
+ libxml_use_internal_errors($internalErrors);
+ libxml_disable_entity_loader($disableEntities);
+
+ foreach ($dom->childNodes as $child) {
+ if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {
+ throw new \InvalidArgumentException('Document types are not allowed.');
+ }
+ }
+
+ if (null !== $schemaOrCallable) {
+ $internalErrors = libxml_use_internal_errors(true);
+ libxml_clear_errors();
+
+ $e = null;
+ if (is_callable($schemaOrCallable)) {
+ try {
+ $valid = call_user_func($schemaOrCallable, $dom, $internalErrors);
+ } catch (\Exception $e) {
+ $valid = false;
+ }
+ } elseif (!is_array($schemaOrCallable) && is_file((string) $schemaOrCallable)) {
+ $valid = @$dom->schemaValidate($schemaOrCallable);
+ } else {
+ libxml_use_internal_errors($internalErrors);
+
+ throw new \InvalidArgumentException('The schemaOrCallable argument has to be a valid path to XSD file or callable.');
+ }
+
+ if (!$valid) {
+ $messages = static::getXmlErrors($internalErrors);
+ if (empty($messages)) {
+ $messages = array(sprintf('The XML file "%s" is not valid.', $file));
+ }
+ throw new \InvalidArgumentException(implode("\n", $messages), 0, $e);
+ }
+
+ libxml_use_internal_errors($internalErrors);
+ }
+
+ return $dom;
+ }
+
+ /**
+ * Converts a \DomElement object to a PHP array.
+ *
+ * The following rules applies during the conversion:
+ *
+ * * Each tag is converted to a key value or an array
+ * if there is more than one "value"
+ *
+ * * The content of a tag is set under a "value" key (bar)
+ * if the tag also has some nested tags
+ *
+ * * The attributes are converted to keys ()
+ *
+ * * The nested-tags are converted to keys (bar)
+ *
+ * @param \DomElement $element A \DomElement instance
+ *
+ * @return array A PHP array
+ */
+ public static function convertDomElementToArray(\DomElement $element)
+ {
+ $empty = true;
+ $config = array();
+ foreach ($element->attributes as $name => $node) {
+ $config[$name] = static::phpize($node->value);
+ $empty = false;
+ }
+
+ $nodeValue = false;
+ foreach ($element->childNodes as $node) {
+ if ($node instanceof \DOMText) {
+ if (trim($node->nodeValue)) {
+ $nodeValue = trim($node->nodeValue);
+ $empty = false;
+ }
+ } elseif (!$node instanceof \DOMComment) {
+ $value = static::convertDomElementToArray($node);
+
+ $key = $node->localName;
+ if (isset($config[$key])) {
+ if (!is_array($config[$key]) || !is_int(key($config[$key]))) {
+ $config[$key] = array($config[$key]);
+ }
+ $config[$key][] = $value;
+ } else {
+ $config[$key] = $value;
+ }
+
+ $empty = false;
+ }
+ }
+
+ if (false !== $nodeValue) {
+ $value = static::phpize($nodeValue);
+ if (count($config)) {
+ $config['value'] = $value;
+ } else {
+ $config = $value;
+ }
+ }
+
+ return !$empty ? $config : null;
+ }
+
+ /**
+ * Converts an xml value to a php type.
+ *
+ * @param mixed $value
+ *
+ * @return mixed
+ */
+ public static function phpize($value)
+ {
+ $value = (string) $value;
+ $lowercaseValue = strtolower($value);
+
+ switch (true) {
+ case 'null' === $lowercaseValue:
+ return null;
+ case ctype_digit($value):
+ $raw = $value;
+ $cast = intval($value);
+
+ return '0' == $value[0] ? octdec($value) : (((string) $raw == (string) $cast) ? $cast : $raw);
+ case 'true' === $lowercaseValue:
+ return true;
+ case 'false' === $lowercaseValue:
+ return false;
+ case is_numeric($value):
+ return '0x' == $value[0].$value[1] ? hexdec($value) : floatval($value);
+ case preg_match('/^(-|\+)?[0-9,]+(\.[0-9]+)?$/', $value):
+ return floatval(str_replace(',', '', $value));
+ default:
+ return $value;
+ }
+ }
+
+ protected static function getXmlErrors($internalErrors)
+ {
+ $errors = array();
+ foreach (libxml_get_errors() as $error) {
+ $errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)',
+ LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR',
+ $error->code,
+ trim($error->message),
+ $error->file ? $error->file : 'n/a',
+ $error->line,
+ $error->column
+ );
+ }
+
+ libxml_clear_errors();
+ libxml_use_internal_errors($internalErrors);
+
+ return $errors;
+ }
+}