From 334434f9de093b2344bb4a294f0d81e1c18db56f Mon Sep 17 00:00:00 2001 From: Jules Pietri Date: Sun, 9 Feb 2020 13:53:06 +0100 Subject: [PATCH] [FrameworkBundle][Routing] added XML and YAML loaders to handle template and redirect controllers --- .../Bundle/FrameworkBundle/CHANGELOG.md | 3 +- .../Resources/config/routing.xml | 4 +- .../config/schema/framework-routing-1.0.xsd | 101 +++++++++ .../Routing/Loader/XmlFileLoader.php | 198 ++++++++++++++++++ .../Routing/Loader/YamlFileLoader.php | 103 +++++++++ .../Resources/config/routing/routes.xml | 54 +++++ .../Resources/config/routing/routes.yaml | 47 +++++ .../config/routing/template_and_redirect.yaml | 4 + .../routing/with_controller_attribute.yaml | 4 + .../Routing/Loader/XmlFileLoaderTest.php | 28 +++ .../Routing/Loader/YamlFileLoaderTest.php | 49 +++++ .../Routing/Loader/XmlFileLoader.php | 8 +- 12 files changed, 596 insertions(+), 7 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/framework-routing-1.0.xsd create mode 100644 src/Symfony/Bundle/FrameworkBundle/Routing/Loader/XmlFileLoader.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Routing/Loader/YamlFileLoader.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/config/routing/routes.xml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/config/routing/routes.yaml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/config/routing/template_and_redirect.yaml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/config/routing/with_controller_attribute.yaml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Routing/Loader/XmlFileLoaderTest.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Routing/Loader/YamlFileLoaderTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index fe887ca9c97bd..c28d8f255d36e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -6,7 +6,8 @@ CHANGELOG * Added link to source for controllers registered as named services * Added link to source on controller on `router:match`/`debug:router` (when `framework.ide` is configured) - * Added `Routing\Loader` and `Routing\Loader\Configurator` namespaces to ease defining routes with default controllers + * Added XML and YAML routing loaders to ease defining routes with redirect and template controllers + * Added the `Routing\Loader\Configurator` namespace to ease defining routes with redirect and template controllers * Added the `framework.router.context` configuration node to configure the `RequestContext` * Made `MicroKernelTrait::configureContainer()` compatible with `ContainerConfigurator` * Added a new `mailer.message_bus` option to configure or disable the message bus to use to send mails. diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml index e4105a59f4626..f7951377e228c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml @@ -15,12 +15,12 @@ - + - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/framework-routing-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/framework-routing-1.0.xsd new file mode 100644 index 0000000000000..43d71756627a3 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/framework-routing-1.0.xsd @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/Loader/XmlFileLoader.php b/src/Symfony/Bundle/FrameworkBundle/Routing/Loader/XmlFileLoader.php new file mode 100644 index 0000000000000..91b0a1e4b432f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/Loader/XmlFileLoader.php @@ -0,0 +1,198 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Routing\Loader; + +use Symfony\Bundle\FrameworkBundle\Controller\RedirectController; +use Symfony\Bundle\FrameworkBundle\Controller\TemplateController; +use Symfony\Component\Config\Util\Exception\InvalidXmlException; +use Symfony\Component\Config\Util\Exception\XmlParsingException; +use Symfony\Component\Config\Util\XmlUtils; +use Symfony\Component\Routing\Loader\XmlFileLoader as BaseXmlFileLoader; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * @author Jules Pietri + */ +class XmlFileLoader extends BaseXmlFileLoader +{ + public const SCHEME_PATH = __DIR__.'/../../Resources/config/schema/framework-routing-1.0.xsd'; + + private const REDEFINED_SCHEME_URI = 'https://symfony.com/schema/routing/routing-1.0.xsd'; + private const SCHEME_URI = 'https://symfony.com/schema/routing/framework-routing-1.0.xsd'; + private const SCHEMA_LOCATIONS = [ + self::REDEFINED_SCHEME_URI => parent::SCHEME_PATH, + self::SCHEME_URI => self::SCHEME_PATH, + ]; + + /** @var \DOMDocument */ + private $document; + + /** + * {@inheritdoc} + */ + protected function loadFile(string $file) + { + if ('' === trim($content = @file_get_contents($file))) { + throw new \InvalidArgumentException(sprintf('File "%s" does not contain valid XML, it is empty.', $file)); + } + + foreach (self::SCHEMA_LOCATIONS as $uri => $path) { + if (false !== strpos($content, $uri)) { + $content = str_replace($uri, self::getRealSchemePath($path), $content); + } + } + + try { + return $this->document = XmlUtils::parse($content, function (\DOMDocument $document) { + return @$document->schemaValidateSource(str_replace( + self::REDEFINED_SCHEME_URI, + self::getRealSchemePath(parent::SCHEME_PATH), + file_get_contents(self::SCHEME_PATH) + )); + }); + } catch (InvalidXmlException $e) { + throw new XmlParsingException(sprintf('The XML file "%s" is not valid.', $file), 0, $e->getPrevious()); + } + } + + /** + * {@inheritdoc} + */ + protected function parseNode(RouteCollection $collection, \DOMElement $node, $path, $file) + { + switch ($node->localName) { + case 'template-route': + case 'redirect-route': + case 'url-redirect-route': + case 'gone-route': + if (self::NAMESPACE_URI !== $node->namespaceURI) { + return; + } + + $this->parseRoute($collection, $node, $path); + + return; + } + + parent::parseNode($collection, $node, $path, $file); + } + + /** + * {@inheritdoc} + */ + protected function parseRoute(RouteCollection $collection, \DOMElement $node, $path) + { + $templateContext = []; + + if ('template-route' === $node->localName) { + /** @var \DOMElement $context */ + foreach ($node->getElementsByTagNameNS(self::NAMESPACE_URI, 'context') as $context) { + $node->removeChild($context); + $map = $this->document->createElementNS(self::NAMESPACE_URI, 'map'); + + // extract context vars into a map + foreach ($context->childNodes as $n) { + if (!$n instanceof \DOMElement) { + continue; + } + + $map->appendChild($n); + } + + $default = $this->document->createElementNS(self::NAMESPACE_URI, 'default'); + $default->setAttribute('key', 'context'); + $default->appendChild($map); + + $templateContext = $this->parseDefaultsConfig($default, $path); + } + } + + parent::parseRoute($collection, $node, $path); + + if ($route = $collection->get($id = $node->getAttribute(('id')))) { + $this->parseConfig($node, $route, $templateContext); + + return; + } + + foreach ($node->getElementsByTagNameNS(self::NAMESPACE_URI, 'path') as $n) { + $route = $collection->get($id.'.'.$n->getAttribute('locale')); + + $this->parseConfig($node, $route, $templateContext); + } + } + + private function parseConfig(\DOMElement $node, Route $route, array $templateContext): void + { + switch ($node->localName) { + case 'template-route': + $route + ->setDefault('_controller', TemplateController::class) + ->setDefault('template', $node->getAttribute('template')) + ->setDefault('context', $templateContext) + ->setDefault('maxAge', (int) $node->getAttribute('max-age') ?: null) + ->setDefault('sharedAge', (int) $node->getAttribute('shared-max-age') ?: null) + ->setDefault('private', $node->hasAttribute('private') ? XmlUtils::phpize($node->getAttribute('private')) : null) + ; + break; + case 'redirect-route': + $route + ->setDefault('_controller', RedirectController::class.'::redirectAction') + ->setDefault('route', $node->getAttribute('redirect-to-route')) + ->setDefault('permanent', self::getBooleanAttribute($node, 'permanent')) + ->setDefault('keepRequestMethod', self::getBooleanAttribute($node, 'keep-request-method')) + ->setDefault('keepQueryParams', self::getBooleanAttribute($node, 'keep-query-params')) + ; + + if (\is_string($ignoreAttributes = XmlUtils::phpize($node->getAttribute('ignore-attributes')))) { + $ignoreAttributes = array_map('trim', explode(',', $ignoreAttributes)); + } + + $route->setDefault('ignoreAttributes', $ignoreAttributes); + break; + case 'url-redirect-route': + $route + ->setDefault('_controller', RedirectController::class.'::urlRedirectAction') + ->setDefault('path', $node->getAttribute('redirect-to-url')) + ->setDefault('permanent', self::getBooleanAttribute($node, 'permanent')) + ->setDefault('scheme', $node->getAttribute('scheme')) + ->setDefault('keepRequestMethod', self::getBooleanAttribute($node, 'keep-request-method')) + ; + if ($node->hasAttribute('http-port')) { + $route->setDefault('httpPort', (int) $node->getAttribute('http-port') ?: null); + } elseif ($node->hasAttribute('https-port')) { + $route->setDefault('httpsPort', (int) $node->getAttribute('https-port') ?: null); + } + break; + case 'gone-route': + $route + ->setDefault('_controller', RedirectController::class.'::redirectAction') + ->setDefault('route', '') + ; + if ($node->hasAttribute('permanent')) { + $route->setDefault('permanent', self::getBooleanAttribute($node, 'permanent')); + } + break; + } + } + + private static function getRealSchemePath(string $schemePath): string + { + return 'file:///'.str_replace('\\', '/', realpath($schemePath)); + } + + private static function getBooleanAttribute(\DOMElement $node, string $attribute): bool + { + return $node->hasAttribute($attribute) ? XmlUtils::phpize($node->getAttribute($attribute)) : false; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/Loader/YamlFileLoader.php b/src/Symfony/Bundle/FrameworkBundle/Routing/Loader/YamlFileLoader.php new file mode 100644 index 0000000000000..ac819897ad4ad --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/Loader/YamlFileLoader.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Routing\Loader; + +use Symfony\Bundle\FrameworkBundle\Controller\RedirectController; +use Symfony\Bundle\FrameworkBundle\Controller\TemplateController; +use Symfony\Component\Routing\Loader\YamlFileLoader as BaseYamlFileLoader; +use Symfony\Component\Routing\RouteCollection; + +/** + * @author Jules Pietri + */ +class YamlFileLoader extends BaseYamlFileLoader +{ + private static $availableKeys = [ + 'template' => ['context', 'max_age', 'shared_max_age', 'private'], + 'redirect_to_route' => ['permanent', 'ignore_attributes', 'keep_request_method', 'keep_query_params'], + 'redirect_to_url' => ['permanent', 'scheme', 'http_port', 'https_port', 'keep_request_method'], + 'gone' => ['permanent'], + ]; + + protected function validate($config, $name, $path) + { + if (\count($types = array_intersect_key($config, self::$availableKeys)) > 1) { + throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify only one route type among "%s" keys for "%s".', str_replace('/', \DIRECTORY_SEPARATOR, $path), implode('", "', array_keys($types)), $name)); + } + + foreach (self::$availableKeys as $routeType => $availableKeys) { + if (!isset($config[$routeType])) { + continue; + } + + if (isset($config['controller'])) { + throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "controller" and the "%s" keys for "%s".', str_replace('/', \DIRECTORY_SEPARATOR, $path), $routeType, $name)); + } + + // keys would be invalid for parent::validate(), but we use them below + unset($config[$routeType]); + foreach ($availableKeys as $key) { + unset($config[$key]); + } + } + + parent::validate($config, $name, $path); + } + + protected function parseRoute(RouteCollection $collection, $name, array $config, $path) + { + if (isset($config['template'])) { + $config['defaults'] = array_merge($config['defaults'] ?? [], [ + '_controller' => TemplateController::class, + 'template' => $config['template'], + 'context' => $config['context'] ?? [], + 'maxAge' => $config['max_age'] ?? null, + 'sharedAge' => $config['shared_max_age'] ?? null, + 'private' => $config['private'] ?? null, + ]); + } elseif (isset($config['redirect_to_route'])) { + $config['defaults'] = array_merge($config['defaults'] ?? [], [ + '_controller' => RedirectController::class.'::redirectAction', + 'route' => $config['redirect_to_route'], + 'permanent' => $config['permanent'] ?? false, + 'ignoreAttributes' => $config['ignore_attributes'] ?? false, + 'keepRequestMethod' => $config['keep_request_method'] ?? false, + 'keepQueryParams' => $config['keep_query_params'] ?? false, + ]); + } elseif (isset($config['redirect_to_url'])) { + $config['defaults'] = array_merge($config['defaults'] ?? [], [ + '_controller' => RedirectController::class.'::urlRedirectAction', + 'path' => $config['redirect_to_url'], + 'permanent' => $config['permanent'] ?? false, + 'scheme' => $config['scheme'] ?? null, + 'keepRequestMethod' => $config['keep_request_method'] ?? false, + ]); + + if (\array_key_exists('http_port', $config)) { + $config['defaults']['httpPort'] = (int) $config['http_port'] ?: null; + } elseif (\array_key_exists('http_port', $config)) { + $config['defaults']['httpsPort'] = (int) $config['https_port'] ?: null; + } + } elseif (isset($config['gone'])) { + $config['defaults'] = array_merge($config['defaults'] ?? [], [ + '_controller' => RedirectController::class.'::redirectAction', + 'route' => '', + ]); + + if (isset($config['permanent'])) { + $config['defaults']['permanent'] = $config['permanent']; + } + } + + parent::parseRoute($collection, $name, $config, $path); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/config/routing/routes.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/config/routing/routes.xml new file mode 100644 index 0000000000000..6392fc5d4a08f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/config/routing/routes.xml @@ -0,0 +1,54 @@ + + + + + + + + bar + + + abc + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/config/routing/routes.yaml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/config/routing/routes.yaml new file mode 100644 index 0000000000000..6369855bc57b0 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/config/routing/routes.yaml @@ -0,0 +1,47 @@ +classic_route: + path: /classic + +template_route: + path: /static + template: static.html.twig + context: + foo: bar + max_age: 300 + shared_max_age: 100 + private: true + methods: GET + options: { utf8: true } + condition: abc + +redirect_route: + path: /redirect + redirect_to_route: target_route + permanent: true + ignore_attributes: ['attr', 'ibutes'] + keep_request_method: true + keep_query_params: true + schemes: http + host: legacy + options: { utf8: true } + +url_redirect_route: + path: /redirect-url + redirect_to_url: /url-target + permanent: true + scheme: http + http_port: 1 + keep_request_method: true + host: legacy + options: { utf8: true } + +not_a_route: + path: /not-a-path + gone: true + host: legacy + options: { utf8: true } + +gone_route: + path: /gone-path + gone: true + permanent: true + options: { utf8: true } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/config/routing/template_and_redirect.yaml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/config/routing/template_and_redirect.yaml new file mode 100644 index 0000000000000..50c2300ee698a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/config/routing/template_and_redirect.yaml @@ -0,0 +1,4 @@ +invalid_route: + path: '/path' + template: 'template.html.twig' + redirect_to_route: 'target_route' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/config/routing/with_controller_attribute.yaml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/config/routing/with_controller_attribute.yaml new file mode 100644 index 0000000000000..7dc728d5571b4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/config/routing/with_controller_attribute.yaml @@ -0,0 +1,4 @@ +invalid_route: + path: '/path' + template: 'template.html.twig' + controller: 'SomeControllerClass' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/Loader/XmlFileLoaderTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/Loader/XmlFileLoaderTest.php new file mode 100644 index 0000000000000..91a28b0aeb355 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/Loader/XmlFileLoaderTest.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Routing\Loader; + +use Symfony\Bundle\FrameworkBundle\Routing\Loader\XmlFileLoader; +use Symfony\Component\Config\Loader\LoaderInterface; + +class XmlFileLoaderTest extends AbstractLoaderTest +{ + protected function getLoader(): LoaderInterface + { + return new XmlFileLoader($this->getLocator()); + } + + protected function getType(): string + { + return 'xml'; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/Loader/YamlFileLoaderTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/Loader/YamlFileLoaderTest.php new file mode 100644 index 0000000000000..0e1a62643e704 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/Loader/YamlFileLoaderTest.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Routing\Loader; + +use Symfony\Bundle\FrameworkBundle\Routing\Loader\YamlFileLoader; +use Symfony\Component\Config\Loader\LoaderInterface; + +class YamlFileLoaderTest extends AbstractLoaderTest +{ + protected function getLoader(): LoaderInterface + { + return new YamlFileLoader($this->getLocator()); + } + + protected function getType(): string + { + return 'yaml'; + } + + /** + * @dataProvider getPathsToInvalidFiles + */ + public function testLoadThrowsExceptionWithInvalidFile(string $filePath, string $exception) + { + $loader = $this->getLoader(); + + $message = sprintf($exception, __DIR__.'/../../Fixtures/Resources/config/routing/'.$filePath); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(str_replace('/', \DIRECTORY_SEPARATOR, $message)); + + $loader->load($filePath); + } + + public function getPathsToInvalidFiles() + { + yield 'defining controller' => ['with_controller_attribute.yaml', 'The routing file "%s" must not specify both the "controller" and the "template" keys for "invalid_route".']; + yield 'defining template and redirect' => ['template_and_redirect.yaml', 'The routing file "%s" must not specify only one route type among "template", "redirect_to_route" keys for "invalid_route".']; + } +} diff --git a/src/Symfony/Component/Routing/Loader/XmlFileLoader.php b/src/Symfony/Component/Routing/Loader/XmlFileLoader.php index 29d3e4a7714d5..ca8564e046050 100644 --- a/src/Symfony/Component/Routing/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Routing/Loader/XmlFileLoader.php @@ -30,7 +30,7 @@ class XmlFileLoader extends FileLoader use PrefixTrait; const NAMESPACE_URI = 'http://symfony.com/schema/routing'; - const SCHEME_PATH = '/schema/routing/routing-1.0.xsd'; + const SCHEME_PATH = __DIR__.'/schema/routing/routing-1.0.xsd'; /** * Loads an XML file. @@ -229,7 +229,7 @@ protected function parseImport(RouteCollection $collection, \DOMElement $node, s */ protected function loadFile(string $file) { - return XmlUtils::loadFile($file, __DIR__.static::SCHEME_PATH); + return XmlUtils::loadFile($file, static::SCHEME_PATH); } /** @@ -303,7 +303,7 @@ private function parseConfigs(\DOMElement $node, string $path): array if (isset($defaults['_stateless'])) { $name = $node->hasAttribute('id') ? sprintf('"%s"', $node->getAttribute('id')) : sprintf('the "%s" tag', $node->tagName); - throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "stateless" attribute and the defaults key "_stateless" for %s.', $path, $name)); + throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "stateless" attribute and the defaults key "_stateless" for "%s".', $path, $name)); } $defaults['_stateless'] = XmlUtils::phpize($stateless); @@ -317,7 +317,7 @@ private function parseConfigs(\DOMElement $node, string $path): array * * @return array|bool|float|int|string|null The parsed value of the "default" element */ - private function parseDefaultsConfig(\DOMElement $element, string $path) + final protected function parseDefaultsConfig(\DOMElement $element, string $path) { if ($this->isElementValueNull($element)) { return null; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy