[DependencyInjection] made the loader extensions much more reliable and robust

This commit is contained in:
Fabien Potencier 2010-01-11 15:56:49 +01:00
parent 0c2b2bdbbb
commit a0551d525c
19 changed files with 276 additions and 151 deletions

View File

@ -165,7 +165,9 @@ class XmlDumper extends Dumper
return <<<EOF
<?xml version="1.0" ?>
<container xmlns="http://symfony-project.org/2.0/container">
<container xmlns="http://www.symfony-project.org/schema/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.symfony-project.org/schema/services http://www.symfony-project.org/schema/services/services-1.0.xsd">
EOF;
}

View File

@ -22,11 +22,23 @@ abstract class Loader implements LoaderInterface
{
static protected $extensions = array();
/**
* Registers an extension.
*
* @param LoaderExtensionInterface $extension An extension instance
*/
static public function registerExtension(LoaderExtensionInterface $extension)
{
static::$extensions[$extension->getNamespace()] = $extension;
static::$extensions[$extension->getAlias()] = static::$extensions[$extension->getNamespace()] = $extension;
}
/**
* Returns an extension by alias or namespace.
*
* @param string $name An alias or a namespace
*
* @return LoaderExtensionInterface An extension instance
*/
static public function getExtension($name)
{
return isset(static::$extensions[$name]) ? static::$extensions[$name] : null;

View File

@ -31,9 +31,18 @@ interface LoaderExtensionInterface
public function load($tag, array $config);
/**
* Returns the namespace to be used for this extension.
* Returns the namespace to be used for this extension (XML namespace).
*
* @return string The namespace
* @return string The XML namespace
*/
public function getNamespace();
/**
* Returns the recommanded alias to use in XML.
*
* This alias is also the mandatory prefix to use when using YAML.
*
* @return string The alias
*/
public function getAlias();
}

View File

@ -195,10 +195,12 @@ class XmlFileLoader extends FileLoader
{
throw new \InvalidArgumentException(implode("\n", $this->getXmlErrors()));
}
$dom->validateOnParse = true;
$dom->normalizeDocument();
libxml_use_internal_errors(false);
$this->validate($dom, $path);
$xmls[$path] = simplexml_import_dom($dom, 'Symfony\Components\DependencyInjection\SimpleXMLElement');
$xmls[$path] = simplexml_import_dom($dom, 'Symfony\\Components\\DependencyInjection\\SimpleXMLElement');
}
return $xmls;
@ -210,7 +212,7 @@ class XmlFileLoader extends FileLoader
$count = 0;
// find anonymous service definitions
$xml->registerXPathNamespace('container', 'http://symfony-project.org/2.0/container');
$xml->registerXPathNamespace('container', 'http://www.symfony-project.org/schema/services');
$nodes = $xml->xpath('//container:argument[@type="service"][not(@id)]');
foreach ($nodes as $node)
{
@ -236,14 +238,52 @@ class XmlFileLoader extends FileLoader
protected function validate($dom, $file)
{
$this->validateSchema($dom, $file);
$this->validateExtensions($dom, $file);
}
protected function validateSchema($dom, $file)
{
$schemaLocations = array('http://www.symfony-project.org/schema/services' => __DIR__.'/schema/services/services-1.0.xsd');
if ($element = $dom->documentElement->getAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'schemaLocation'))
{
$items = preg_split('/\s+/', $element);
for ($i = 0, $nb = count($items); $i < $nb; $i += 2)
{
$schemaLocations[$items[$i]] = str_replace('http://www.symfony-project.org/', __DIR__.'/', $items[$i + 1]);
}
}
$imports = '';
foreach ($schemaLocations as $namespace => $location)
{
$imports .= sprintf(' <xsd:import namespace="%s" schemaLocation="%s" />'."\n", $namespace, $location);
}
$source = <<<EOF
<?xml version="1.0" encoding="utf-8" ?>
<xsd:schema xmlns="http://www.symfony-project.org/schema"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.symfony-project.org/schema"
elementFormDefault="qualified">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
$imports
</xsd:schema>
EOF
;
libxml_use_internal_errors(true);
if (!$dom->schemaValidate(__DIR__.'/services.xsd'))
if (!$dom->schemaValidateSource($source))
{
throw new \InvalidArgumentException(implode("\n", $this->getXmlErrors()));
}
libxml_use_internal_errors(false);
}
// validate extensions
protected function validateExtensions($dom, $file)
{
foreach ($dom->documentElement->childNodes as $node)
{
if (!$node instanceof \DOMElement || in_array($node->tagName, array('imports', 'parameters', 'services')))
@ -251,19 +291,16 @@ class XmlFileLoader extends FileLoader
continue;
}
// can it be handled by an extension?
if (false !== strpos($node->tagName, ':'))
if ($node->namespaceURI === 'http://www.symfony-project.org/schema/services')
{
list($namespace, $tag) = explode(':', $node->tagName);
if (!static::getExtension($namespace))
{
throw new \InvalidArgumentException(sprintf('There is no extension able to load the configuration for "%s" (in %s).', $node->tagName, $file));
}
continue;
throw new \InvalidArgumentException(sprintf('The "%s" tag is not valid (in %s).', $node->tagName, $file));
}
throw new \InvalidArgumentException(sprintf('The "%s" tag is not valid (in %s).', $node->tagName, $file));
// can it be handled by an extension?
if (!static::getExtension($node->namespaceURI))
{
throw new \InvalidArgumentException(sprintf('There is no extension able to load the configuration for "%s" (in %s).', $node->tagName, $file));
}
}
}
@ -289,15 +326,15 @@ class XmlFileLoader extends FileLoader
protected function loadFromExtensions(BuilderConfiguration $configuration, $xml)
{
foreach (dom_import_simplexml($xml)->getElementsByTagNameNS('*', '*') as $element)
foreach (dom_import_simplexml($xml)->childNodes as $node)
{
if (!$element->prefix)
if (!$node instanceof \DOMElement || $node->namespaceURI === 'http://www.symfony-project.org/schema/services')
{
continue;
}
$values = static::convertDomElementToArray($element);
$config = $this->getExtension($element->prefix)->load($element->localName, is_array($values) ? $values : array($values));
$values = static::convertDomElementToArray($node);
$config = $this->getExtension($node->namespaceURI)->load($node->localName, is_array($values) ? $values : array($values));
$configuration->merge($config);
}
@ -345,7 +382,7 @@ class XmlFileLoader extends FileLoader
}
elseif (!$node instanceof \DOMComment)
{
$config[$node->tagName] = static::convertDomElementToArray($node);
$config[$node->localName] = static::convertDomElementToArray($node);
$empty = false;
}
}

View File

@ -0,0 +1,155 @@
<?xml version="1.0" encoding="UTF-8" ?>
<xsd:schema xmlns="http://www.symfony-project.org/schema/services"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.symfony-project.org/schema/services"
elementFormDefault="qualified">
<xsd:annotation>
<xsd:documentation><![CDATA[
Symfony XML Services Schema, version 1.0
Authors: Fabien Potencier
This defines a way to describe PHP objects (services) and their
dependencies.
]]></xsd:documentation>
</xsd:annotation>
<xsd:element name="container" type="container" />
<xsd:complexType name="container">
<xsd:annotation>
<xsd:documentation><![CDATA[
The root element of a service file.
]]></xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:any namespace="##any" processContents="lax" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="imports" type="imports" minOccurs="0" maxOccurs="1" />
<xsd:element name="parameters" type="parameters" minOccurs="0" maxOccurs="1" />
<xsd:element name="services" type="services" minOccurs="0" maxOccurs="1" />
<xsd:any namespace="##any" processContents="lax" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="services">
<xsd:annotation>
<xsd:documentation><![CDATA[
Enclosing element for the definition of all services
]]></xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:element name="service" type="service" minOccurs="1" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="imports">
<xsd:annotation>
<xsd:documentation><![CDATA[
Enclosing element for the import elements
]]></xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:element name="import" type="import" minOccurs="1" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="import">
<xsd:annotation>
<xsd:documentation><![CDATA[
Import an external resource defining other services or parameters
]]></xsd:documentation>
</xsd:annotation>
<xsd:attribute name="resource" type="xsd:string" use="required" />
<xsd:attribute name="class" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
The PHP class able to load the resource. If not defined, the loader uses the current loader.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="configurator">
<xsd:attribute name="id" type="xsd:string" />
<xsd:attribute name="service" type="xsd:string" />
<xsd:attribute name="class" type="xsd:string" />
<xsd:attribute name="method" type="xsd:string" />
<xsd:attribute name="function" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="service">
<xsd:choice maxOccurs="unbounded">
<xsd:element name="file" type="xsd:string" minOccurs="0" maxOccurs="1" />
<xsd:element name="argument" type="argument" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="configurator" type="configurator" minOccurs="0" maxOccurs="1" />
<xsd:element name="call" type="call" minOccurs="0" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="id" type="xsd:string" />
<xsd:attribute name="class" type="xsd:string" />
<xsd:attribute name="shared" type="boolean" />
<xsd:attribute name="constructor" type="xsd:string" />
<xsd:attribute name="alias" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="parameters">
<xsd:sequence>
<xsd:element name="parameter" type="parameter" minOccurs="1" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="type" type="parameter_type" />
<xsd:attribute name="key" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="parameter" mixed="true">
<xsd:sequence>
<xsd:element name="parameter" type="parameter" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="type" type="parameter_type" />
<xsd:attribute name="id" type="xsd:string" />
<xsd:attribute name="key" type="xsd:string" />
<xsd:attribute name="on-invalid" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="argument" mixed="true">
<xsd:choice maxOccurs="unbounded">
<xsd:element name="argument" type="argument" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="service" type="service" />
</xsd:choice>
<xsd:attribute name="type" type="argument_type" />
<xsd:attribute name="id" type="xsd:string" />
<xsd:attribute name="key" type="xsd:string" />
<xsd:attribute name="on-invalid" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="call" mixed="true">
<xsd:choice maxOccurs="unbounded">
<xsd:element name="argument" type="argument" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="service" type="service" />
</xsd:choice>
<xsd:attribute name="method" type="xsd:string" />
</xsd:complexType>
<xsd:simpleType name="parameter_type">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="collection" />
<xsd:enumeration value="service" />
<xsd:enumeration value="string" />
<xsd:enumeration value="constant" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="argument_type">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="collection" />
<xsd:enumeration value="service" />
<xsd:enumeration value="string" />
<xsd:enumeration value="constant" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="boolean">
<xsd:restriction base="xsd:string">
<xsd:pattern value="(%.+%|true|false)" />
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

View File

@ -1,113 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://symfony-project.org/2.0/container" targetNamespace="http://symfony-project.org/2.0/container" elementFormDefault="qualified">
<xs:element name="container" type="container" />
<xs:complexType name="container">
<xs:sequence>
<xs:element name="imports" type="imports" minOccurs="0" maxOccurs="1" />
<xs:element name="parameters" type="parameters" minOccurs="0" maxOccurs="1" />
<xs:element name="services" type="services" minOccurs="0" maxOccurs="1" />
<xs:any namespace="##any" processContents="skip" minOccurs="0" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="services">
<xs:sequence>
<xs:element name="service" type="service" minOccurs="1" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="imports">
<xs:sequence>
<xs:element name="import" type="import" minOccurs="1" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="import">
<xs:attribute name="resource" type="xs:string" use="required" />
<xs:attribute name="class" type="xs:string" />
</xs:complexType>
<xs:complexType name="configurator">
<xs:attribute name="id" type="xs:string" />
<xs:attribute name="service" type="xs:string" />
<xs:attribute name="class" type="xs:string" />
<xs:attribute name="method" type="xs:string" />
<xs:attribute name="function" type="xs:string" />
</xs:complexType>
<xs:complexType name="service">
<xs:choice maxOccurs="unbounded">
<xs:element name="file" type="xs:string" minOccurs="0" maxOccurs="1" />
<xs:element name="argument" type="argument" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="configurator" type="configurator" minOccurs="0" maxOccurs="1" />
<xs:element name="call" type="call" minOccurs="0" maxOccurs="unbounded" />
</xs:choice>
<xs:attribute name="id" type="xs:string" />
<xs:attribute name="class" type="xs:string" />
<xs:attribute name="shared" type="boolean" />
<xs:attribute name="constructor" type="xs:string" />
<xs:attribute name="alias" type="xs:string" />
</xs:complexType>
<xs:complexType name="parameters">
<xs:sequence>
<xs:element name="parameter" type="parameter" minOccurs="1" maxOccurs="unbounded" />
</xs:sequence>
<xs:attribute name="type" type="parameter_type" />
<xs:attribute name="key" type="xs:string" />
</xs:complexType>
<xs:complexType name="parameter" mixed="true">
<xs:sequence>
<xs:element name="parameter" type="parameter" minOccurs="0" maxOccurs="unbounded" />
</xs:sequence>
<xs:attribute name="type" type="parameter_type" />
<xs:attribute name="id" type="xs:string" />
<xs:attribute name="key" type="xs:string" />
<xs:attribute name="on-invalid" type="xs:string" />
</xs:complexType>
<xs:complexType name="argument" mixed="true">
<xs:choice maxOccurs="unbounded">
<xs:element name="argument" type="argument" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="service" type="service" />
</xs:choice>
<xs:attribute name="type" type="argument_type" />
<xs:attribute name="id" type="xs:string" />
<xs:attribute name="key" type="xs:string" />
<xs:attribute name="on-invalid" type="xs:string" />
</xs:complexType>
<xs:complexType name="call" mixed="true">
<xs:choice maxOccurs="unbounded">
<xs:element name="argument" type="argument" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="service" type="service" />
</xs:choice>
<xs:attribute name="method" type="xs:string" />
</xs:complexType>
<xs:simpleType name="parameter_type">
<xs:restriction base="xs:string">
<xs:enumeration value="collection" />
<xs:enumeration value="service" />
<xs:enumeration value="string" />
<xs:enumeration value="constant" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="argument_type">
<xs:restriction base="xs:string">
<xs:enumeration value="collection" />
<xs:enumeration value="service" />
<xs:enumeration value="string" />
<xs:enumeration value="constant" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="boolean">
<xs:restriction base="xs:string">
<xs:pattern value="(%.+%|true|false)" />
</xs:restriction>
</xs:simpleType>
</xs:schema>

View File

@ -17,6 +17,11 @@ class ProjectExtension extends LoaderExtension
}
public function getNamespace()
{
return 'http://www.example.com/schema/project';
}
public function getAlias()
{
return 'project';
}

View File

@ -1,4 +1,6 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony-project.org/2.0/container">
<container xmlns="http://www.symfony-project.org/schema/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.symfony-project.org/schema/services http://www.symfony-project.org/schema/services/services-1.0.xsd">
</container>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony-project.org/2.0/container"
xmlns:project="http://symfony-project.org/2.0/container/project">
<container xmlns="http://www.symfony-project.org/schema/services"
xmlns:project="http://www.example.com/schema/project">
<project:bar />

View File

@ -1,7 +1,7 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony-project.org/2.0/container"
xmlns:foobar="http://symfony-project.org/2.0/container/foobar">
<container xmlns="http://www.symfony-project.org/schema/services"
xmlns:foobar="http://www.example.com/schema/foobar">
<foobar:foobar />

View File

@ -1,7 +1,7 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony-project.org/2.0/container"
xmlns:project="http://symfony-project.org/2.0/container/project">
<container xmlns="http://www.symfony-project.org/schema/services"
xmlns:project="http://www.example.com/schema/project">
<foobar />

View File

@ -1,6 +1,8 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony-project.org/2.0/container">
<container xmlns="http://www.symfony-project.org/schema/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.symfony-project.org/schema/services http://www.symfony-project.org/schema/services/services-1.0.xsd">
<parameters>
<parameter>a string</parameter>
<parameter key="FOO">bar</parameter>

View File

@ -1,6 +1,8 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony-project.org/2.0/container">
<container xmlns="http://www.symfony-project.org/schema/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.symfony-project.org/schema/services http://www.symfony-project.org/schema/services/services-1.0.xsd">
<parameters>
<parameter key="foo">foo</parameter>
<parameter key="values" type="collection">

View File

@ -1,6 +1,8 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony-project.org/2.0/container">
<container xmlns="http://www.symfony-project.org/schema/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.symfony-project.org/schema/services http://www.symfony-project.org/schema/services/services-1.0.xsd">
<imports>
<import resource="services2.xml" />
<import resource="services3.xml" />

View File

@ -1,6 +1,8 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony-project.org/2.0/container">
<container xmlns="http://www.symfony-project.org/schema/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.symfony-project.org/schema/services http://www.symfony-project.org/schema/services/services-1.0.xsd">
<services>
<service id="foo" class="FooClass">
<argument type="service">

View File

@ -1,6 +1,8 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony-project.org/2.0/container">
<container xmlns="http://www.symfony-project.org/schema/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.symfony-project.org/schema/services http://www.symfony-project.org/schema/services/services-1.0.xsd">
<services>
<service id="foo" class="FooClass" />
<service id="baz" class="BazClass" />

View File

@ -1,6 +1,8 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony-project.org/2.0/container">
<container xmlns="http://www.symfony-project.org/schema/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.symfony-project.org/schema/services http://www.symfony-project.org/schema/services/services-1.0.xsd">
<services>
<service id="foo" class="BarClass" />
</services>

View File

@ -1,6 +1,8 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony-project.org/2.0/container">
<container xmlns="http://www.symfony-project.org/schema/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.symfony-project.org/schema/services http://www.symfony-project.org/schema/services/services-1.0.xsd">
<parameters>
<parameter key="foo">bar</parameter>
<parameter key="bar">foo is %%foo bar</parameter>

View File

@ -1,6 +1,8 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony-project.org/2.0/container">
<container xmlns="http://www.symfony-project.org/schema/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.symfony-project.org/schema/services http://www.symfony-project.org/schema/services/services-1.0.xsd">
<parameters>
<parameter key="baz_class">BazClass</parameter>
<parameter key="foo_class">FooClass</parameter>