This repository has been archived on 2023-08-20. You can view files and clone it, but cannot push or open issues or pull requests.
symfony/src/Symfony/Component/Routing/Loader/XmlFileLoader.php

259 lines
9.6 KiB
PHP
Raw Normal View History

2010-02-17 13:53:31 +00:00
<?php
/*
* This file is part of the Symfony package.
2010-02-17 13:53:31 +00:00
*
* (c) Fabien Potencier <fabien@symfony.com>
2010-02-17 13:53:31 +00:00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
2010-02-17 13:53:31 +00:00
*/
namespace Symfony\Component\Routing\Loader;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Config\Util\XmlUtils;
2010-02-17 13:53:31 +00:00
/**
* XmlFileLoader loads XML routing files.
*
* @author Fabien Potencier <fabien@symfony.com>
2012-12-03 11:51:35 +00:00
* @author Tobias Schultze <http://tobion.de>
2011-06-14 14:35:32 +01:00
*
* @api
2010-02-17 13:53:31 +00:00
*/
class XmlFileLoader extends FileLoader
{
const NAMESPACE_URI = 'http://symfony.com/schema/routing';
const SCHEME_PATH = '/schema/routing/routing-1.0.xsd';
/**
* Loads an XML file.
*
* @param string $file An XML file path
* @param string|null $type The resource type
*
* @return RouteCollection A RouteCollection instance
*
2012-12-03 11:51:35 +00:00
* @throws \InvalidArgumentException When the file cannot be loaded or when the XML cannot be
* parsed because it does not validate against the scheme.
2011-06-14 14:35:32 +01:00
*
* @api
*/
public function load($file, $type = null)
2010-02-17 13:53:31 +00:00
{
refactored bundle management Before I explain the changes, let's talk about the current state. Before this patch, the registerBundleDirs() method returned an ordered (for resource overloading) list of namespace prefixes and the path to their location. Here are some problems with this approach: * The paths set by this method and the paths configured for the autoloader can be disconnected (leading to unexpected behaviors); * A bundle outside these paths worked, but unexpected behavior can occur; * Choosing a bundle namespace was limited to the registered namespace prefixes, and their number should stay low enough (for performance reasons) -- moreover the current Bundle\ and Application\ top namespaces does not respect the standard rules for namespaces (first segment should be the vendor name); * Developers must understand the concept of "namespace prefixes" to understand the overloading mechanism, which is one more thing to learn, which is Symfony specific; * Each time you want to get a resource that can be overloaded (a template for instance), Symfony would have tried all namespace prefixes one after the other until if finds a matching file. But that can be computed in advance to reduce the overhead. Another topic which was not really well addressed is how you can reference a file/resource from a bundle (and take into account the possibility of overloading). For instance, in the routing, you can import a file from a bundle like this: <import resource="FrameworkBundle/Resources/config/internal.xml" /> Again, this works only because we have a limited number of possible namespace prefixes. This patch addresses these problems and some more. First, the registerBundleDirs() method has been removed. It means that you are now free to use any namespace for your bundles. No need to have specific prefixes anymore. You are also free to store them anywhere, in as many directories as you want. You just need to be sure that they are autoloaded correctly. The bundle "name" is now always the short name of the bundle class (like FrameworkBundle or SensioCasBundle). As the best practice is to prefix the bundle name with the vendor name, it's up to the vendor to ensure that each bundle name is unique. I insist that a bundle name must be unique. This was the opposite before as two bundles with the same name was how Symfony2 found inheritance. A new getParent() method has been added to BundleInterface. It returns the bundle name that the bundle overrides (this is optional of course). That way, there is no ordering problem anymore as the inheritance tree is explicitely defined by the bundle themselves. So, with this system, we can easily have an inheritance tree like the following: FooBundle < MyFooBundle < MyCustomFooBundle MyCustomFooBundle returns MyFooBundle for the getParent() method, and MyFooBundle returns FooBundle. If two bundles override the same bundle, an exception is thrown. Based on the bundle name, you can now reference any resource with this notation: @FooBundle/Resources/config/routing.xml @FooBundle/Controller/FooController.php This notation is the input of the Kernel::locateResource() method, which returns the location of the file (and of course it takes into account overloading). So, in the routing, you can now use the following: <import resource="@FrameworkBundle/Resources/config/internal.xml" /> The template loading mechanism also use this method under the hood. As a bonus, all the code that converts from internal notations to file names (controller names: ControllerNameParser, template names: TemplateNameParser, resource paths, ...) is now contained in several well-defined classes. The same goes for the code that look for templates (TemplateLocator), routing files (FileLocator), ... As a side note, it is really easy to also support multiple-inheritance for a bundle (for instance if a bundle returns an array of bundle names it extends). However, this is not implemented in this patch as I'm not sure we want to support that. How to upgrade: * Each bundle must now implement two new mandatory methods: getPath() and getNamespace(), and optionally the getParent() method if the bundle extends another one. Here is a common implementation for these methods: /** * {@inheritdoc} */ public function getParent() { return 'MyFrameworkBundle'; } /** * {@inheritdoc} */ public function getNamespace() { return __NAMESPACE__; } /** * {@inheritdoc} */ public function getPath() { return strtr(__DIR__, '\\', '/'); } * The registerBundleDirs() can be removed from your Kernel class; * If your code relies on getBundleDirs() or the kernel.bundle_dirs parameter, it should be upgraded to use the new interface (see Doctrine commands for many example of such a change); * When referencing a bundle, you must now always use its name (no more \ or / in bundle names) -- this transition was already done for most things before, and now applies to the routing as well; * Imports in routing files must be changed: Before: <import resource="Sensio/CasBundle/Resources/config/internal.xml" /> After: <import resource="@SensioCasBundle/Resources/config/internal.xml" />
2011-01-18 09:23:49 +00:00
$path = $this->locator->locate($file);
$xml = $this->loadFile($path);
$collection = new RouteCollection();
$collection->addResource(new FileResource($path));
// process routes and imports
foreach ($xml->documentElement->childNodes as $node) {
if (!$node instanceof \DOMElement) {
continue;
}
2011-05-26 17:28:42 +01:00
$this->parseNode($collection, $node, $path, $file);
}
return $collection;
2010-02-17 13:53:31 +00:00
}
/**
* Parses a node from a loaded XML file.
*
2012-12-03 11:51:35 +00:00
* @param RouteCollection $collection Collection to associate with the node
* @param \DOMElement $node Element to parse
* @param string $path Full path of the XML file being processed
* @param string $file Loaded file name
*
2012-12-03 11:51:35 +00:00
* @throws \InvalidArgumentException When the XML is invalid
*/
2011-05-26 17:28:42 +01:00
protected function parseNode(RouteCollection $collection, \DOMElement $node, $path, $file)
{
if (self::NAMESPACE_URI !== $node->namespaceURI) {
return;
}
switch ($node->localName) {
2011-05-26 17:27:18 +01:00
case 'route':
$this->parseRoute($collection, $node, $path);
break;
case 'import':
2012-12-03 11:51:35 +00:00
$this->parseImport($collection, $node, $path, $file);
2011-05-26 17:27:18 +01:00
break;
default:
2012-12-03 11:51:35 +00:00
throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "route" or "import".', $node->localName, $path));
2011-05-26 17:27:18 +01:00
}
}
/**
* {@inheritdoc}
2011-06-14 14:35:32 +01:00
*
* @api
*/
public function supports($resource, $type = null)
{
return is_string($resource) && 'xml' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'xml' === $type);
}
/**
* Parses a route and adds it to the RouteCollection.
*
2012-12-03 11:51:35 +00:00
* @param RouteCollection $collection RouteCollection instance
* @param \DOMElement $node Element to parse that represents a Route
* @param string $path Full path of the XML file being processed
*
2012-12-03 11:51:35 +00:00
* @throws \InvalidArgumentException When the XML is invalid
*/
2012-12-03 11:51:35 +00:00
protected function parseRoute(RouteCollection $collection, \DOMElement $node, $path)
2010-02-17 13:53:31 +00:00
{
2013-01-14 16:36:16 +00:00
if ('' === ($id = $node->getAttribute('id')) || (!$node->hasAttribute('pattern') && !$node->hasAttribute('path'))) {
throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must have an "id" and a "path" attribute.', $path));
}
2013-01-14 16:36:16 +00:00
if ($node->hasAttribute('pattern')) {
if ($node->hasAttribute('path')) {
throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" cannot define both a "path" and a "pattern" attribute. Use only "path".', $path));
}
trigger_error(sprintf('The "pattern" in file "%s" is deprecated since version 2.2 and will be removed in 3.0. Use the "path" option in the route definition instead.', $path), E_USER_DEPRECATED);
2013-01-14 16:36:16 +00:00
$node->setAttribute('path', $node->getAttribute('pattern'));
$node->removeAttribute('pattern');
}
$schemes = preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, PREG_SPLIT_NO_EMPTY);
$methods = preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, PREG_SPLIT_NO_EMPTY);
list($defaults, $requirements, $options, $condition) = $this->parseConfigs($node, $path);
$route = new Route($node->getAttribute('path'), $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods, $condition);
$collection->add($id, $route);
2012-12-03 11:51:35 +00:00
}
/**
* Parses an import and adds the routes in the resource to the RouteCollection.
*
* @param RouteCollection $collection RouteCollection instance
* @param \DOMElement $node Element to parse that represents a Route
* @param string $path Full path of the XML file being processed
* @param string $file Loaded file name
*
* @throws \InvalidArgumentException When the XML is invalid
*/
protected function parseImport(RouteCollection $collection, \DOMElement $node, $path, $file)
{
if ('' === $resource = $node->getAttribute('resource')) {
throw new \InvalidArgumentException(sprintf('The <import> element in file "%s" must have a "resource" attribute.', $path));
}
2012-12-03 11:51:35 +00:00
$type = $node->getAttribute('type');
$prefix = $node->getAttribute('prefix');
$host = $node->hasAttribute('host') ? $node->getAttribute('host') : null;
$schemes = $node->hasAttribute('schemes') ? preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, PREG_SPLIT_NO_EMPTY) : null;
$methods = $node->hasAttribute('methods') ? preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, PREG_SPLIT_NO_EMPTY) : null;
2012-12-03 11:51:35 +00:00
list($defaults, $requirements, $options, $condition) = $this->parseConfigs($node, $path);
2012-12-03 11:51:35 +00:00
$this->setCurrentDir(dirname($path));
$subCollection = $this->import($resource, ('' !== $type ? $type : null), false, $file);
/* @var $subCollection RouteCollection */
$subCollection->addPrefix($prefix);
if (null !== $host) {
$subCollection->setHost($host);
2012-12-03 11:51:35 +00:00
}
2014-01-09 15:21:09 +00:00
if (null !== $condition) {
$subCollection->setCondition($condition);
}
if (null !== $schemes) {
$subCollection->setSchemes($schemes);
}
if (null !== $methods) {
$subCollection->setMethods($methods);
2012-12-03 11:51:35 +00:00
}
$subCollection->addDefaults($defaults);
$subCollection->addRequirements($requirements);
$subCollection->addOptions($options);
2012-12-03 11:51:35 +00:00
$collection->addCollection($subCollection);
2010-02-17 13:53:31 +00:00
}
/**
* Loads an XML file.
*
* @param string $file An XML file path
*
* @return \DOMDocument
*
* @throws \InvalidArgumentException When loading of XML file fails because of syntax errors
* or when the XML structure is not as expected by the scheme -
* see validate()
*/
protected function loadFile($file)
2010-02-17 13:53:31 +00:00
{
return XmlUtils::loadFile($file, __DIR__.static::SCHEME_PATH);
2010-02-17 13:53:31 +00:00
}
2012-12-03 11:51:35 +00:00
/**
* Parses the config elements (default, requirement, option).
*
* @param \DOMElement $node Element to parse that contains the configs
* @param string $path Full path of the XML file being processed
*
* @return array An array with the defaults as first item, requirements as second and options as third.
*
* @throws \InvalidArgumentException When the XML is invalid
*/
private function parseConfigs(\DOMElement $node, $path)
{
$defaults = array();
$requirements = array();
$options = array();
$condition = null;
2012-12-03 11:51:35 +00:00
foreach ($node->getElementsByTagNameNS(self::NAMESPACE_URI, '*') as $n) {
2012-12-03 11:51:35 +00:00
switch ($n->localName) {
case 'default':
if ($this->isElementValueNull($n)) {
$defaults[$n->getAttribute('key')] = null;
} else {
$defaults[$n->getAttribute('key')] = trim($n->textContent);
}
2012-12-03 11:51:35 +00:00
break;
case 'requirement':
$requirements[$n->getAttribute('key')] = trim($n->textContent);
break;
case 'option':
$options[$n->getAttribute('key')] = trim($n->textContent);
break;
case 'condition':
$condition = trim($n->textContent);
break;
2012-12-03 11:51:35 +00:00
default:
throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "default", "requirement" or "option".', $n->localName, $path));
}
}
return array($defaults, $requirements, $options, $condition);
2012-12-03 11:51:35 +00:00
}
private function isElementValueNull(\DOMElement $element)
{
$namespaceUri = 'http://www.w3.org/2001/XMLSchema-instance';
if (!$element->hasAttributeNS($namespaceUri, 'nil')) {
return false;
}
return 'true' === $element->getAttributeNS($namespaceUri, 'nil') || '1' === $element->getAttributeNS($namespaceUri, 'nil');
}
2010-02-17 13:53:31 +00:00
}