feature #30501 [FrameworkBundle][Routing] added Configurators to handle template and redirect controllers (HeahDude)

This PR was merged into the 5.1-dev branch.

Discussion
----------

[FrameworkBundle][Routing] added Configurators to handle template and redirect controllers

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | let's see
| Fixed tickets | partially #24640, #25145
| License       | MIT
| Doc PR        | symfony/symfony-docs#11120

While working on https://github.com/symfony/symfony-docs/pull/11085, I felt bad about the long notations required for simple [redirects](https://symfony.com/doc/current/routing/redirect_in_config.html) and [templates rendering](https://symfony.com/doc/current/templating/render_without_controller.html) template actions, but I love and use those features since always. Then I gave it a try yesterday night and now I realised I missed #24640 and that #25145 has been closed x).

So here we go, here's my WIP. WDYT of this implementation? ping @javiereguiluz?

I'm going to open the PR in the docs so we can discuss the DX changes there too, and keep focus on the code here.

Cheers!

EDIT
----
This PR now only update PHP-DSL configurators.

______________

TODO:

- [x] gather reviews
- ~[x] fix xml schema~
- [x] add some tests
- ~[ ] handle xsd auto discovery~
- [x] rebase on top of #30507
- [x] ~add shortcuts for #30514~

Commits
-------

de74794acf [FrameworkBundle][Routing] added Configurators to handle template and redirect controllers
This commit is contained in:
Nicolas Grekas 2020-02-09 17:37:38 +01:00
commit 477ee19778
25 changed files with 788 additions and 205 deletions

View File

@ -4,6 +4,7 @@ CHANGELOG
5.1.0
-----
* Added `Routing\Loader` and `Routing\Loader\Configurator` namespaces to ease defining routes with default 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.
@ -29,7 +30,7 @@ CHANGELOG
* Removed the `translator.selector` and `session.save_listener` services
* Removed `SecurityUserValueResolver`, use `UserValueResolver` instead
* Removed `routing.loader.service`.
* Service route loaders must be tagged with `routing.route_loader`.
* Service route loaders must be tagged with `routing.route_loader`.
* Added `slugger` service and `SluggerInterface` alias
* Removed the `lock.store.flock`, `lock.store.semaphore`, `lock.store.memcached.abstract` and `lock.store.redis.abstract` services.
* Removed the `router.cache_class_prefix` parameter.
@ -81,8 +82,8 @@ CHANGELOG
options if you're using Symfony's serializer.
* [BC Break] Removed the `framework.messenger.routing.send_and_handle` configuration.
Instead of setting it to true, configure a `SyncTransport` and route messages to it.
* Added information about deprecated aliases in `debug:autowiring`
* Added php ini session options `sid_length` and `sid_bits_per_character`
* Added information about deprecated aliases in `debug:autowiring`
* Added php ini session options `sid_length` and `sid_bits_per_character`
to the `session` section of the configuration
* Added support for Translator paths, Twig paths in translation commands.
* Added support for PHP files with translations in translation commands.

View File

@ -11,14 +11,14 @@
namespace Symfony\Bundle\FrameworkBundle\Kernel;
use Symfony\Bundle\FrameworkBundle\Routing\Loader\Configurator\RoutingConfigurator;
use Symfony\Bundle\FrameworkBundle\Routing\Loader\PhpFileLoader as RoutingPhpFileLoader;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\AbstractConfigurator;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader as ContainerPhpFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
use Symfony\Component\Routing\Loader\PhpFileLoader as RoutingPhpFileLoader;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\RouteCollectionBuilder;

View File

@ -25,7 +25,7 @@
<argument type="service" id="file_locator" />
</service>
<service id="routing.loader.php" class="Symfony\Component\Routing\Loader\PhpFileLoader">
<service id="routing.loader.php" class="Symfony\Bundle\FrameworkBundle\Routing\Loader\PhpFileLoader">
<tag name="routing.loader" />
<argument type="service" id="file_locator" />
</service>

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Configurator;
use Symfony\Bundle\FrameworkBundle\Routing\Loader\Configurator\Traits\AddTrait;
use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class GoneRouteConfigurator extends RouteConfigurator
{
use AddTrait;
/**
* @param bool $permanent Whether the redirection is permanent
*
* @return $this
*/
final public function permanent(bool $permanent = true)
{
return $this->defaults(['permanent' => $permanent]);
}
}

View File

@ -0,0 +1,63 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Configurator;
use Symfony\Bundle\FrameworkBundle\Routing\Loader\Configurator\Traits\AddTrait;
use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator;
/**
* @author Jules Pietri <jules@heahprod.com>
*/
class RedirectRouteConfigurator extends RouteConfigurator
{
use AddTrait;
/**
* @param bool $permanent Whether the redirection is permanent
*
* @return $this
*/
final public function permanent(bool $permanent = true)
{
return $this->defaults(['permanent' => $permanent]);
}
/**
* @param bool|array $ignoreAttributes Whether to ignore attributes or an array of attributes to ignore
*
* @return $this
*/
final public function ignoreAttributes($ignoreAttributes = true)
{
return $this->defaults(['ignoreAttributes' => $ignoreAttributes]);
}
/**
* @param bool $keepRequestMethod Whether redirect action should keep HTTP request method
*
* @return $this
*/
final public function keepRequestMethod(bool $keepRequestMethod = true)
{
return $this->defaults(['keepRequestMethod' => $keepRequestMethod]);
}
/**
* @param bool $keepQueryParams Whether redirect action should keep query parameters
*
* @return $this
*/
final public function keepQueryParams(bool $keepQueryParams = true)
{
return $this->defaults(['keepQueryParams' => $keepQueryParams]);
}
}

View File

@ -0,0 +1,73 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Configurator;
use Symfony\Bundle\FrameworkBundle\Controller\RedirectController;
use Symfony\Bundle\FrameworkBundle\Controller\TemplateController;
use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator as BaseRouteConfigurator;
/**
* @author Jules Pietri <jules@heahprod.com>
*/
class RouteConfigurator extends BaseRouteConfigurator
{
/**
* @param string $template The template name
* @param array $context The template variables
*/
final public function template(string $template, array $context = []): TemplateRouteConfigurator
{
return (new TemplateRouteConfigurator($this->collection, $this->route, $this->name, $this->parentConfigurator, $this->prefixes))
->defaults([
'_controller' => TemplateController::class,
'template' => $template,
'context' => $context,
])
;
}
/**
* @param string $route The route name to redirect to
*/
final public function redirectToRoute(string $route): RedirectRouteConfigurator
{
return (new RedirectRouteConfigurator($this->collection, $this->route, $this->name, $this->parentConfigurator, $this->prefixes))
->defaults([
'_controller' => RedirectController::class.'::redirectAction',
'route' => $route,
])
;
}
/**
* @param string $url The relative path or URL to redirect to
*/
final public function redirectToUrl(string $url): UrlRedirectRouteConfigurator
{
return (new UrlRedirectRouteConfigurator($this->collection, $this->route, $this->name, $this->parentConfigurator, $this->prefixes))
->defaults([
'_controller' => RedirectController::class.'::urlRedirectAction',
'path' => $url,
])
;
}
final public function gone(): GoneRouteConfigurator
{
return (new GoneRouteConfigurator($this->collection, $this->route, $this->name, $this->parentConfigurator, $this->prefixes))
->defaults([
'_controller' => RedirectController::class.'::redirectAction',
'route' => '',
])
;
}
}

View File

@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Configurator;
use Symfony\Bundle\FrameworkBundle\Routing\Loader\Configurator\Traits\AddTrait;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator as BaseRoutingConfigurator;
class RoutingConfigurator extends BaseRoutingConfigurator
{
use AddTrait;
}

View File

@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Configurator;
use Symfony\Bundle\FrameworkBundle\Routing\Loader\Configurator\Traits\AddTrait;
use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator;
/**
* @author Jules Pietri <jules@heahprod.com>
*/
class TemplateRouteConfigurator extends RouteConfigurator
{
use AddTrait;
/**
* @param int|null $maxAge Max age for client caching
*
* @return $this
*/
final public function maxAge(?int $maxAge)
{
return $this->defaults(['maxAge' => $maxAge]);
}
/**
* @param int|null $sharedMaxAge Max age for shared (proxy) caching
*
* @return $this
*/
final public function sharedMaxAge(?int $sharedMaxAge)
{
return $this->defaults(['sharedAge' => $sharedMaxAge]);
}
/**
* @param bool|null $private Whether or not caching should apply for client caches only
*
* @return $this
*/
final public function private(?bool $private = true)
{
return $this->defaults(['private' => $private]);
}
}

View File

@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Configurator\Traits;
use Symfony\Bundle\FrameworkBundle\Routing\Loader\Configurator\RouteConfigurator;
use Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator;
use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator as BaseRouteConfigurator;
trait AddTrait
{
/**
* Adds a route.
*
* @param string|array $path the path, or the localized paths of the route
*
* @return RouteConfigurator
*/
public function add(string $name, $path): BaseRouteConfigurator
{
$parentConfigurator = $this instanceof CollectionConfigurator ? $this : ($this instanceof RouteConfigurator ? $this->parentConfigurator : null);
$route = $this->createLocalizedRoute($this->collection, $name, $path, $this->name, $this->prefixes);
return new RouteConfigurator($this->collection, $route, $this->name, $parentConfigurator, $this->prefixes);
}
/**
* Adds a route.
*
* @param string|array $path the path, or the localized paths of the route
*
* @return RouteConfigurator
*/
final public function __invoke(string $name, $path): BaseRouteConfigurator
{
return $this->add($name, $path);
}
}

View File

@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Configurator;
use Symfony\Bundle\FrameworkBundle\Routing\Loader\Configurator\Traits\AddTrait;
use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator;
/**
* @author Jules Pietri <jules@heahprod.com>
*/
class UrlRedirectRouteConfigurator extends RouteConfigurator
{
use AddTrait;
/**
* @param bool $permanent Whether the redirection is permanent
*
* @return $this
*/
final public function permanent(bool $permanent = true)
{
return $this->defaults(['permanent' => $permanent]);
}
/**
* @param string|null $scheme The URL scheme (null to keep the current one)
* @param int|null $port The HTTP or HTTPS port (null to keep the current one for the same scheme or the default configured port)
*
* @return $this
*/
final public function scheme(?string $scheme, int $port = null)
{
$this->defaults(['scheme' => $scheme]);
if ('http' === $scheme) {
$this->defaults(['httpPort' => $port]);
} elseif ('https' === $scheme) {
$this->defaults(['httpsPort' => $port]);
}
return $this;
}
/**
* @param bool $keepRequestMethod Whether redirect action should keep HTTP request method
*
* @return $this
*/
final public function keepRequestMethod(bool $keepRequestMethod = true)
{
return $this->defaults(['keepRequestMethod' => $keepRequestMethod]);
}
}

View File

@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Routing\Loader\Configurator\RoutingConfigurator;
use Symfony\Component\Routing\Loader\PhpFileLoader as BasePhpFileLoader;
use Symfony\Component\Routing\RouteCollection;
/**
* @author Jules Pietri <jules@heahprod.com>
*/
class PhpFileLoader extends BasePhpFileLoader
{
protected function callConfigurator(callable $result, string $path, string $file): RouteCollection
{
$collection = new RouteCollection();
$result(new RoutingConfigurator($collection, $this, $path, $file));
return $collection;
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace Symfony\Bundle\FrameworkBundle\Routing\Loader\Configurator;
return function (RoutingConfigurator $routes) {
$routes->add('classic_route', '/classic');
$routes->add('template_route', '/static')
->template('static.html.twig', ['foo' => 'bar'])
->maxAge(300)
->sharedMaxAge(100)
->private()
->methods(['GET'])
->utf8()
->condition('abc')
;
$routes->add('redirect_route', '/redirect')
->redirectToRoute('target_route')
->permanent()
->ignoreAttributes(['attr', 'ibutes'])
->keepRequestMethod()
->keepQueryParams()
->schemes(['http'])
->host('legacy')
->utf8()
;
$routes->add('url_redirect_route', '/redirect-url')
->redirectToUrl('/url-target')
->permanent()
->scheme('http', 1)
->keepRequestMethod()
->host('legacy')
->utf8()
;
$routes->add('not_a_route', '/not-a-path')
->gone()
->host('legacy')
->utf8()
;
$routes->add('gone_route', '/gone-path')
->gone()
->permanent()
->utf8()
;
};

View File

@ -0,0 +1,116 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Controller\RedirectController;
use Symfony\Bundle\FrameworkBundle\Controller\TemplateController;
use Symfony\Bundle\FrameworkBundle\Tests\TestCase;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\FileLocatorInterface;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
abstract class AbstractLoaderTest extends TestCase
{
/** @var LoaderInterface */
protected $loader;
abstract protected function getLoader(): LoaderInterface;
abstract protected function getType(): string;
protected function setUp(): void
{
$this->loader = $this->getLoader();
}
protected function tearDown(): void
{
$this->loader = null;
}
public function getLocator(): FileLocatorInterface
{
return new FileLocator([__DIR__.'/../../Fixtures/Resources/config/routing']);
}
public function testRoutesAreLoaded()
{
$routeCollection = $this->loader->load('routes.'.$this->getType());
$expectedCollection = new RouteCollection();
$expectedCollection->add('classic_route', (new Route('/classic')));
$expectedCollection->add('template_route', (new Route('/static'))
->setDefaults([
'_controller' => TemplateController::class,
'context' => ['foo' => 'bar'],
'template' => 'static.html.twig',
'maxAge' => 300,
'sharedAge' => 100,
'private' => true,
])
->setMethods(['GET'])
->setOptions(['utf8' => true])
->setCondition('abc')
);
$expectedCollection->add('redirect_route', (new Route('/redirect'))
->setDefaults([
'_controller' => RedirectController::class.'::redirectAction',
'route' => 'target_route',
'permanent' => true,
'ignoreAttributes' => ['attr', 'ibutes'],
'keepRequestMethod' => true,
'keepQueryParams' => true,
])
->setSchemes(['http'])
->setHost('legacy')
->setOptions(['utf8' => true])
);
$expectedCollection->add('url_redirect_route', (new Route('/redirect-url'))
->setDefaults([
'_controller' => RedirectController::class.'::urlRedirectAction',
'path' => '/url-target',
'permanent' => true,
'scheme' => 'http',
'httpPort' => 1,
'keepRequestMethod' => true,
])
->setHost('legacy')
->setOptions(['utf8' => true])
);
$expectedCollection->add('not_a_route', (new Route('/not-a-path'))
->setDefaults([
'_controller' => RedirectController::class.'::redirectAction',
'route' => '',
])
->setHost('legacy')
->setOptions(['utf8' => true])
);
$expectedCollection->add('gone_route', (new Route('/gone-path'))
->setDefaults([
'_controller' => RedirectController::class.'::redirectAction',
'route' => '',
'permanent' => true,
])
->setOptions(['utf8' => true])
);
$expectedCollection->addResource(new FileResource(realpath(
__DIR__.'/../../Fixtures/Resources/config/routing/routes.'.$this->getType()
)));
$this->assertEquals($expectedCollection, $routeCollection);
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\PhpFileLoader;
use Symfony\Component\Config\Loader\LoaderInterface;
class PhpFileLoaderTest extends AbstractLoaderTest
{
protected function getLoader(): LoaderInterface
{
return new PhpFileLoader($this->getLocator());
}
protected function getType(): string
{
return 'php';
}
}

View File

@ -80,6 +80,7 @@
"symfony/messenger": "<4.4",
"symfony/mime": "<4.4",
"symfony/property-info": "<4.4",
"symfony/routing": "<5.1",
"symfony/serializer": "<4.4",
"symfony/stopwatch": "<4.4",
"symfony/translation": "<5.0",

View File

@ -4,6 +4,7 @@ CHANGELOG
5.1.0
-----
* added the protected method `PhpFileLoader::callConfigurator()` as extension point to ease custom routing configuration
* deprecated `RouteCollectionBuilder` in favor of `RoutingConfigurator`.
* added "priority" option to annotated routes
* added argument `$priority` to `RouteCollection::add()`

View File

@ -11,7 +11,6 @@
namespace Symfony\Component\Routing\Loader\Configurator;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
@ -19,6 +18,7 @@ use Symfony\Component\Routing\RouteCollection;
*/
class ImportConfigurator
{
use Traits\PrefixTrait;
use Traits\RouteTrait;
private $parent;
@ -43,38 +43,7 @@ class ImportConfigurator
*/
final public function prefix($prefix, bool $trailingSlashOnRoot = true): self
{
if (!\is_array($prefix)) {
$this->route->addPrefix($prefix);
if (!$trailingSlashOnRoot) {
$rootPath = (new Route(trim(trim($prefix), '/').'/'))->getPath();
foreach ($this->route->all() as $route) {
if ($route->getPath() === $rootPath) {
$route->setPath(rtrim($rootPath, '/'));
}
}
}
} else {
foreach ($prefix as $locale => $localePrefix) {
$prefix[$locale] = trim(trim($localePrefix), '/');
}
foreach ($this->route->all() as $name => $route) {
if (null === $locale = $route->getDefault('_locale')) {
$this->route->remove($name);
foreach ($prefix as $locale => $localePrefix) {
$localizedRoute = clone $route;
$localizedRoute->setDefault('_locale', $locale);
$localizedRoute->setDefault('_canonical_route', $name);
$localizedRoute->setPath($localePrefix.(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
$this->route->add($name.'.'.$locale, $localizedRoute);
}
} elseif (!isset($prefix[$locale])) {
throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale));
} else {
$route->setPath($prefix[$locale].(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
$this->route->add($name, $route);
}
}
}
$this->addPrefix($this->route, $prefix, $trailingSlashOnRoot);
return $this;
}

View File

@ -21,7 +21,7 @@ class RouteConfigurator
use Traits\AddTrait;
use Traits\RouteTrait;
private $parentConfigurator;
protected $parentConfigurator;
public function __construct(RouteCollection $collection, $route, string $name = '', CollectionConfigurator $parentConfigurator = null, array $prefixes = null)
{

View File

@ -13,64 +13,33 @@ namespace Symfony\Component\Routing\Loader\Configurator\Traits;
use Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator;
use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
trait AddTrait
{
use LocalizedRouteTrait;
/**
* @var RouteCollection
*/
private $collection;
private $name = '';
private $prefixes;
protected $collection;
protected $name = '';
protected $prefixes;
/**
* Adds a route.
*
* @param string|array $path the path, or the localized paths of the route
*/
final public function add(string $name, $path): RouteConfigurator
public function add(string $name, $path): RouteConfigurator
{
$paths = [];
$parentConfigurator = $this instanceof CollectionConfigurator ? $this : ($this instanceof RouteConfigurator ? $this->parentConfigurator : null);
$route = $this->createLocalizedRoute($this->collection, $name, $path, $this->name, $this->prefixes);
if (\is_array($path)) {
if (null === $this->prefixes) {
$paths = $path;
} elseif ($missing = array_diff_key($this->prefixes, $path)) {
throw new \LogicException(sprintf('Route "%s" is missing routes for locale(s) "%s".', $name, implode('", "', array_keys($missing))));
} else {
foreach ($path as $locale => $localePath) {
if (!isset($this->prefixes[$locale])) {
throw new \LogicException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale));
}
$paths[$locale] = $this->prefixes[$locale].$localePath;
}
}
} elseif (null !== $this->prefixes) {
foreach ($this->prefixes as $locale => $prefix) {
$paths[$locale] = $prefix.$path;
}
} else {
$this->collection->add($this->name.$name, $route = $this->createRoute($path));
return new RouteConfigurator($this->collection, $route, $this->name, $parentConfigurator, $this->prefixes);
}
$routes = new RouteCollection();
foreach ($paths as $locale => $path) {
$routes->add($name.'.'.$locale, $route = $this->createRoute($path));
$this->collection->add($this->name.$name.'.'.$locale, $route);
$route->setDefault('_locale', $locale);
$route->setDefault('_canonical_route', $this->name.$name);
}
return new RouteConfigurator($this->collection, $routes, $this->name, $parentConfigurator, $this->prefixes);
return new RouteConfigurator($this->collection, $route, $this->name, $parentConfigurator, $this->prefixes);
}
/**
@ -78,13 +47,8 @@ trait AddTrait
*
* @param string|array $path the path, or the localized paths of the route
*/
final public function __invoke(string $name, $path): RouteConfigurator
public function __invoke(string $name, $path): RouteConfigurator
{
return $this->add($name, $path);
}
private function createRoute(string $path): Route
{
return new Route($path);
}
}

View File

@ -0,0 +1,74 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Loader\Configurator\Traits;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* @author Nicolas Grekas <p@tchwork.com>
* @author Jules Pietri <jules@heahprod.com>
*/
trait LocalizedRouteTrait
{
/**
* Creates one or many routes.
*
* @param string|array $path the path, or the localized paths of the route
*
* @return Route|RouteCollection
*/
final protected function createLocalizedRoute(RouteCollection $collection, string $name, $path, string $namePrefix = '', array $prefixes = null)
{
$paths = [];
if (\is_array($path)) {
if (null === $prefixes) {
$paths = $path;
} elseif ($missing = array_diff_key($prefixes, $path)) {
throw new \LogicException(sprintf('Route "%s" is missing routes for locale(s) "%s".', $name, implode('", "', array_keys($missing))));
} else {
foreach ($path as $locale => $localePath) {
if (!isset($prefixes[$locale])) {
throw new \LogicException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale));
}
$paths[$locale] = $prefixes[$locale].$localePath;
}
}
} elseif (null !== $prefixes) {
foreach ($prefixes as $locale => $prefix) {
$paths[$locale] = $prefix.$path;
}
} else {
$collection->add($namePrefix.$name, $route = $this->createRoute($path));
return $route;
}
$routes = new RouteCollection();
foreach ($paths as $locale => $path) {
$routes->add($name.'.'.$locale, $route = $this->createRoute($path));
$collection->add($namePrefix.$name.'.'.$locale, $route);
$route->setDefault('_locale', $locale);
$route->setDefault('_canonical_route', $namePrefix.$name);
}
return $routes;
}
private function createRoute(string $path): Route
{
return new Route($path);
}
}

View File

@ -0,0 +1,59 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Routing\Loader\Configurator\Traits;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
trait PrefixTrait
{
final protected function addPrefix(RouteCollection $routes, $prefix, bool $trailingSlashOnRoot)
{
if (\is_array($prefix)) {
foreach ($prefix as $locale => $localePrefix) {
$prefix[$locale] = trim(trim($localePrefix), '/');
}
foreach ($routes->all() as $name => $route) {
if (null === $locale = $route->getDefault('_locale')) {
$routes->remove($name);
foreach ($prefix as $locale => $localePrefix) {
$localizedRoute = clone $route;
$localizedRoute->setDefault('_locale', $locale);
$localizedRoute->setDefault('_canonical_route', $name);
$localizedRoute->setPath($localePrefix.(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
$routes->add($name.'.'.$locale, $localizedRoute);
}
} elseif (!isset($prefix[$locale])) {
throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale));
} else {
$route->setPath($prefix[$locale].(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
$routes->add($name, $route);
}
}
return;
}
$routes->addPrefix($prefix);
if (!$trailingSlashOnRoot) {
$rootPath = (new Route(trim(trim($prefix), '/').'/'))->getPath();
foreach ($routes->all() as $route) {
if ($route->getPath() === $rootPath) {
$route->setPath(rtrim($rootPath, '/'));
}
}
}
}
}

View File

@ -19,7 +19,7 @@ trait RouteTrait
/**
* @var RouteCollection|Route
*/
private $route;
protected $route;
/**
* Adds defaults.

View File

@ -22,6 +22,8 @@ use Symfony\Component\Routing\RouteCollection;
* The file must return a RouteCollection instance.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Nicolas grekas <p@tchwork.com>
* @author Jules Pietri <jules@heahprod.com>
*/
class PhpFileLoader extends FileLoader
{
@ -47,8 +49,7 @@ class PhpFileLoader extends FileLoader
$result = $load($path);
if (\is_object($result) && \is_callable($result)) {
$collection = new RouteCollection();
$result(new RoutingConfigurator($collection, $this, $path, $file));
$collection = $this->callConfigurator($result, $path, $file);
} else {
$collection = $result;
}
@ -65,6 +66,15 @@ class PhpFileLoader extends FileLoader
{
return \is_string($resource) && 'php' === pathinfo($resource, PATHINFO_EXTENSION) && (!$type || 'php' === $type);
}
protected function callConfigurator(callable $result, string $path, string $file): RouteCollection
{
$collection = new RouteCollection();
$result(new RoutingConfigurator($collection, $this, $path, $file));
return $collection;
}
}
/**

View File

@ -14,7 +14,8 @@ namespace Symfony\Component\Routing\Loader;
use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\Loader\Configurator\Traits\LocalizedRouteTrait;
use Symfony\Component\Routing\Loader\Configurator\Traits\PrefixTrait;
use Symfony\Component\Routing\RouteCollection;
/**
@ -25,6 +26,9 @@ use Symfony\Component\Routing\RouteCollection;
*/
class XmlFileLoader extends FileLoader
{
use LocalizedRouteTrait;
use PrefixTrait;
const NAMESPACE_URI = 'http://symfony.com/schema/routing';
const SCHEME_PATH = '/schema/routing/routing-1.0.xsd';
@ -98,41 +102,40 @@ class XmlFileLoader extends FileLoader
/**
* Parses a route and adds it to the RouteCollection.
*
* @param \DOMElement $node Element to parse that represents a Route
* @param string $path Full path of the XML file being processed
* @param \DOMElement $node Element to parse that represents a Route
* @param string $filepath Full path of the XML file being processed
*
* @throws \InvalidArgumentException When the XML is invalid
*/
protected function parseRoute(RouteCollection $collection, \DOMElement $node, string $path)
protected function parseRoute(RouteCollection $collection, \DOMElement $node, string $filepath)
{
if ('' === $id = $node->getAttribute('id')) {
throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must have an "id" attribute.', $path));
throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must have an "id" attribute.', $filepath));
}
$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, $paths) = $this->parseConfigs($node, $path);
list($defaults, $requirements, $options, $condition, $paths) = $this->parseConfigs($node, $filepath);
if (!$paths && '' === $node->getAttribute('path')) {
throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must have a "path" attribute or <path> child nodes.', $path));
$path = $node->getAttribute('path');
if (!$paths && '' === $path) {
throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must have a "path" attribute or <path> child nodes.', $filepath));
}
if ($paths && '' !== $node->getAttribute('path')) {
throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must not have both a "path" attribute and <path> child nodes.', $path));
if ($paths && '' !== $path) {
throw new \InvalidArgumentException(sprintf('The <route> element in file "%s" must not have both a "path" attribute and <path> child nodes.', $filepath));
}
if (!$paths) {
$route = new Route($node->getAttribute('path'), $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods, $condition);
$collection->add($id, $route);
} else {
foreach ($paths as $locale => $p) {
$defaults['_locale'] = $locale;
$defaults['_canonical_route'] = $id;
$route = new Route($p, $defaults, $requirements, $options, $node->getAttribute('host'), $schemes, $methods, $condition);
$collection->add($id.'.'.$locale, $route);
}
}
$route = $this->createLocalizedRoute($collection, $id, $paths ?: $path);
$route->addDefaults($defaults);
$route->addRequirements($requirements);
$route->addOptions($options);
$route->setHost($node->getAttribute('host'));
$route->setSchemes($schemes);
$route->setMethods($methods);
$route->setCondition($condition);
}
/**
@ -156,6 +159,7 @@ class XmlFileLoader extends FileLoader
$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;
$trailingSlashOnRoot = $node->hasAttribute('trailing-slash-on-root') ? XmlUtils::phpize($node->getAttribute('trailing-slash-on-root')) : true;
$namePrefix = $node->getAttribute('name-prefix') ?: null;
list($defaults, $requirements, $options, $condition, /* $paths */, $prefixes) = $this->parseConfigs($node, $path);
@ -187,39 +191,7 @@ class XmlFileLoader extends FileLoader
}
foreach ($imported as $subCollection) {
/* @var $subCollection RouteCollection */
if ('' !== $prefix || !$prefixes) {
$subCollection->addPrefix($prefix);
if (!$trailingSlashOnRoot) {
$rootPath = (new Route(trim(trim($prefix), '/').'/'))->getPath();
foreach ($subCollection->all() as $route) {
if ($route->getPath() === $rootPath) {
$route->setPath(rtrim($rootPath, '/'));
}
}
}
} else {
foreach ($prefixes as $locale => $localePrefix) {
$prefixes[$locale] = trim(trim($localePrefix), '/');
}
foreach ($subCollection->all() as $name => $route) {
if (null === $locale = $route->getDefault('_locale')) {
$subCollection->remove($name);
foreach ($prefixes as $locale => $localePrefix) {
$localizedRoute = clone $route;
$localizedRoute->setPath($localePrefix.(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
$localizedRoute->setDefault('_locale', $locale);
$localizedRoute->setDefault('_canonical_route', $name);
$subCollection->add($name.'.'.$locale, $localizedRoute);
}
} elseif (!isset($prefixes[$locale])) {
throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix when imported in "%s".', $name, $locale, $path));
} else {
$route->setPath($prefixes[$locale].(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
$subCollection->add($name, $route);
}
}
}
$this->addPrefix($subCollection, $prefixes ?: $prefix, $trailingSlashOnRoot);
if (null !== $host) {
$subCollection->setHost($host);
@ -233,14 +205,13 @@ class XmlFileLoader extends FileLoader
if (null !== $methods) {
$subCollection->setMethods($methods);
}
if (null !== $namePrefix) {
$subCollection->addNamePrefix($namePrefix);
}
$subCollection->addDefaults($defaults);
$subCollection->addRequirements($requirements);
$subCollection->addOptions($options);
if ($namePrefix = $node->getAttribute('name-prefix')) {
$subCollection->addNamePrefix($namePrefix);
}
$collection->addCollection($subCollection);
}
}

View File

@ -13,7 +13,8 @@ namespace Symfony\Component\Routing\Loader;
use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\Loader\Configurator\Traits\LocalizedRouteTrait;
use Symfony\Component\Routing\Loader\Configurator\Traits\PrefixTrait;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Parser as YamlParser;
@ -27,6 +28,9 @@ use Symfony\Component\Yaml\Yaml;
*/
class YamlFileLoader extends FileLoader
{
use LocalizedRouteTrait;
use PrefixTrait;
private static $availableKeys = [
'resource', 'type', 'prefix', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options', 'condition', 'controller', 'name_prefix', 'trailing_slash_on_root', 'locale', 'format', 'utf8', 'exclude',
];
@ -110,10 +114,6 @@ class YamlFileLoader extends FileLoader
$defaults = isset($config['defaults']) ? $config['defaults'] : [];
$requirements = isset($config['requirements']) ? $config['requirements'] : [];
$options = isset($config['options']) ? $config['options'] : [];
$host = isset($config['host']) ? $config['host'] : '';
$schemes = isset($config['schemes']) ? $config['schemes'] : [];
$methods = isset($config['methods']) ? $config['methods'] : [];
$condition = isset($config['condition']) ? $config['condition'] : null;
foreach ($requirements as $placeholder => $requirement) {
if (\is_int($placeholder)) {
@ -134,20 +134,14 @@ class YamlFileLoader extends FileLoader
$options['utf8'] = $config['utf8'];
}
if (\is_array($config['path'])) {
$route = new Route('', $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
foreach ($config['path'] as $locale => $path) {
$localizedRoute = clone $route;
$localizedRoute->setDefault('_locale', $locale);
$localizedRoute->setDefault('_canonical_route', $name);
$localizedRoute->setPath($path);
$collection->add($name.'.'.$locale, $localizedRoute);
}
} else {
$route = new Route($config['path'], $defaults, $requirements, $options, $host, $schemes, $methods, $condition);
$collection->add($name, $route);
}
$route = $this->createLocalizedRoute($collection, $name, $config['path']);
$route->addDefaults($defaults);
$route->addRequirements($requirements);
$route->addOptions($options);
$route->setHost($config['host'] ?? '');
$route->setSchemes($config['schemes'] ?? []);
$route->setMethods($config['methods'] ?? []);
$route->setCondition($config['condition'] ?? null);
}
/**
@ -169,6 +163,7 @@ class YamlFileLoader extends FileLoader
$schemes = isset($config['schemes']) ? $config['schemes'] : null;
$methods = isset($config['methods']) ? $config['methods'] : null;
$trailingSlashOnRoot = $config['trailing_slash_on_root'] ?? true;
$namePrefix = $config['name_prefix'] ?? '';
$exclude = $config['exclude'] ?? null;
if (isset($config['controller'])) {
@ -186,6 +181,7 @@ class YamlFileLoader extends FileLoader
$this->setCurrentDir(\dirname($path));
/** @var RouteCollection[] $imported */
$imported = $this->import($config['resource'], $type, false, $file, $exclude) ?: [];
if (!\is_array($imported)) {
@ -193,39 +189,7 @@ class YamlFileLoader extends FileLoader
}
foreach ($imported as $subCollection) {
/* @var $subCollection RouteCollection */
if (!\is_array($prefix)) {
$subCollection->addPrefix($prefix);
if (!$trailingSlashOnRoot) {
$rootPath = (new Route(trim(trim($prefix), '/').'/'))->getPath();
foreach ($subCollection->all() as $route) {
if ($route->getPath() === $rootPath) {
$route->setPath(rtrim($rootPath, '/'));
}
}
}
} else {
foreach ($prefix as $locale => $localePrefix) {
$prefix[$locale] = trim(trim($localePrefix), '/');
}
foreach ($subCollection->all() as $name => $route) {
if (null === $locale = $route->getDefault('_locale')) {
$subCollection->remove($name);
foreach ($prefix as $locale => $localePrefix) {
$localizedRoute = clone $route;
$localizedRoute->setDefault('_locale', $locale);
$localizedRoute->setDefault('_canonical_route', $name);
$localizedRoute->setPath($localePrefix.(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
$subCollection->add($name.'.'.$locale, $localizedRoute);
}
} elseif (!isset($prefix[$locale])) {
throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix when imported in "%s".', $name, $locale, $file));
} else {
$route->setPath($prefix[$locale].(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath()));
$subCollection->add($name, $route);
}
}
}
$this->addPrefix($subCollection, $prefix, $trailingSlashOnRoot);
if (null !== $host) {
$subCollection->setHost($host);
@ -239,14 +203,13 @@ class YamlFileLoader extends FileLoader
if (null !== $methods) {
$subCollection->setMethods($methods);
}
if (null !== $namePrefix) {
$subCollection->addNamePrefix($namePrefix);
}
$subCollection->addDefaults($defaults);
$subCollection->addRequirements($requirements);
$subCollection->addOptions($options);
if (isset($config['name_prefix'])) {
$subCollection->addNamePrefix($config['name_prefix']);
}
$collection->addCollection($subCollection);
}
}