diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 133a784e0d26..877b4abeedc5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Allowed to pass an optional `LoggerInterface $logger` instance to the `Router` * Added a new `parameter_bag` service with related autowiring aliases to access parameters as-a-service + * Allowed the `Router` to work with any PSR-11 container 4.0.0 ----- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 414f2ad7d2c8..725ae02a1a0f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -619,6 +619,13 @@ private function registerRouterConfiguration(array $config, ContainerBuilder $co $loader->load('routing.xml'); + if (!interface_exists(ContainerBagInterface::class)) { + $container->getDefinition('router.default') + ->replaceArgument(0, new Reference('service_container')) + ->clearTag('container.service_subscriber') + ; + } + $container->setParameter('router.resource', $config['resource']); $container->setParameter('router.cache_class_prefix', $container->getParameter('kernel.container_class')); $router = $container->findDefinition('router.default'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml index 81af5c397c86..5d25c9116f35 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml @@ -52,7 +52,8 @@ - + + %router.resource% %kernel.cache_dir% @@ -67,6 +68,7 @@ %router.cache_class_prefix%UrlMatcher + diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php b/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php index 232b39283cac..055609c48857 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php @@ -11,13 +11,14 @@ namespace Symfony\Bundle\FrameworkBundle\Routing; +use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\Config\ContainerParametersResource; +use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface; use Symfony\Component\DependencyInjection\ServiceSubscriberInterface; use Symfony\Component\Routing\Router as BaseRouter; use Symfony\Component\Routing\RequestContext; -use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException; @@ -32,22 +33,31 @@ class Router extends BaseRouter implements WarmableInterface, ServiceSubscriberI { private $container; private $collectedParameters = array(); + private $paramFetcher; /** - * @param ContainerInterface $container A ContainerInterface instance - * @param mixed $resource The main resource to load - * @param array $options An array of options - * @param RequestContext $context The context - * @param LoggerInterface|null $logger + * @param ContainerInterface $container A ContainerInterface instance + * @param mixed $resource The main resource to load + * @param array $options An array of options + * @param RequestContext $context The context + * @param ContainerInterface|null $parameters A ContainerInterface instance allowing to fetch parameters + * @param LoggerInterface|null $logger */ - public function __construct(ContainerInterface $container, $resource, array $options = array(), RequestContext $context = null, LoggerInterface $logger = null) + public function __construct(ContainerInterface $container, $resource, array $options = array(), RequestContext $context = null, ContainerInterface $parameters = null, LoggerInterface $logger = null) { $this->container = $container; - $this->resource = $resource; $this->context = $context ?: new RequestContext(); $this->logger = $logger; $this->setOptions($options); + + if ($parameters) { + $this->paramFetcher = array($parameters, 'get'); + } elseif ($container instanceof SymfonyContainerInterface) { + $this->paramFetcher = array($container, 'getParameter'); + } else { + throw new \LogicException(sprintf('You should either pass a "%s" instance or provide the $parameters argument of the "%s" method.', SymfonyContainerInterface::class, __METHOD__)); + } } /** @@ -142,9 +152,7 @@ private function resolve($value) return $value; } - $container = $this->container; - - $escapedValue = preg_replace_callback('/%%|%([^%\s]++)%/', function ($match) use ($container, $value) { + $escapedValue = preg_replace_callback('/%%|%([^%\s]++)%/', function ($match) use ($value) { // skip %% if (!isset($match[1])) { return '%%'; @@ -154,7 +162,7 @@ private function resolve($value) throw new RuntimeException(sprintf('Using "%%%s%%" is not allowed in routing configuration.', $match[1])); } - $resolved = $container->getParameter($match[1]); + $resolved = ($this->paramFetcher)($match[1]); if (is_string($resolved) || is_numeric($resolved)) { $this->collectedParameters[$match[1]] = $resolved; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php index e0548078fca2..f686ec59d66d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php @@ -12,13 +12,24 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Routing; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; use Symfony\Bundle\FrameworkBundle\Routing\Router; +use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\Config\ContainerParametersResource; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; class RouterTest extends TestCase { + /** + * @expectedException \LogicException + * @expectedExceptionMessage You should either pass a "Symfony\Component\DependencyInjection\ContainerInterface" instance or provide the $parameters argument of the "Symfony\Bundle\FrameworkBundle\Routing\Router::__construct" method + */ + public function testConstructThrowsOnNonSymfonyNorPsr11Container() + { + new Router($this->getMockBuilder(ContainerInterface::class)->getMock(), 'foo'); + } + public function testGenerateWithServiceParam() { $routes = new RouteCollection(); @@ -33,6 +44,33 @@ public function testGenerateWithServiceParam() ), array(), '', array(), array(), '"%foo%" == "bar"' )); + $sc = $this->getPsr11ServiceContainer($routes); + $parameters = $this->getParameterBag(array( + 'locale' => 'es', + 'foo' => 'bar', + )); + + $router = new Router($sc, 'foo', array(), null, $parameters); + + $this->assertSame('/en', $router->generate('foo', array('_locale' => 'en'))); + $this->assertSame('/', $router->generate('foo', array('_locale' => 'es'))); + $this->assertSame('"bar" == "bar"', $router->getRouteCollection()->get('foo')->getCondition()); + } + + public function testGenerateWithServiceParamWithSfContainer() + { + $routes = new RouteCollection(); + + $routes->add('foo', new Route( + ' /{_locale}', + array( + '_locale' => '%locale%', + ), + array( + '_locale' => 'en|es', + ), array(), '', array(), array(), '"%foo%" == "bar"' + )); + $sc = $this->getServiceContainer($routes); $sc->setParameter('locale', 'es'); $sc->setParameter('foo', 'bar'); @@ -61,6 +99,47 @@ public function testDefaultsPlaceholders() ) )); + $sc = $this->getPsr11ServiceContainer($routes); + + $parameters = $this->getParameterBag(array( + 'parameter.foo' => 'foo', + 'parameter.bar' => 'bar', + 'parameter' => 'boo', + 'bee_parameter' => 'foo_bee', + )); + + $router = new Router($sc, 'foo', array(), null, $parameters); + $route = $router->getRouteCollection()->get('foo'); + + $this->assertEquals( + array( + 'foo' => 'before_foo', + 'bar' => 'bar_after', + 'baz' => '%escaped%', + 'boo' => array('boo', '%escaped_parameter%', array('foo_bee', 'bee')), + 'bee' => array('bee', 'bee'), + ), + $route->getDefaults() + ); + } + + public function testDefaultsPlaceholdersWithSfContainer() + { + $routes = new RouteCollection(); + + $routes->add('foo', new Route( + '/foo', + array( + 'foo' => 'before_%parameter.foo%', + 'bar' => '%parameter.bar%_after', + 'baz' => '%%escaped%%', + 'boo' => array('%parameter%', '%%escaped_parameter%%', array('%bee_parameter%', 'bee')), + 'bee' => array('bee', 'bee'), + ), + array( + ) + )); + $sc = $this->getServiceContainer($routes); $sc->setParameter('parameter.foo', 'foo'); @@ -98,6 +177,41 @@ public function testRequirementsPlaceholders() ) )); + $sc = $this->getPsr11ServiceContainer($routes); + $parameters = $this->getParameterBag(array( + 'parameter.foo' => 'foo', + 'parameter.bar' => 'bar', + )); + + $router = new Router($sc, 'foo', array(), null, $parameters); + + $route = $router->getRouteCollection()->get('foo'); + + $this->assertEquals( + array( + 'foo' => 'before_foo', + 'bar' => 'bar_after', + 'baz' => '%escaped%', + ), + $route->getRequirements() + ); + } + + public function testRequirementsPlaceholdersWithSfContainer() + { + $routes = new RouteCollection(); + + $routes->add('foo', new Route( + '/foo', + array( + ), + array( + 'foo' => 'before_%parameter.foo%', + 'bar' => '%parameter.bar%_after', + 'baz' => '%%escaped%%', + ) + )); + $sc = $this->getServiceContainer($routes); $sc->setParameter('parameter.foo', 'foo'); $sc->setParameter('parameter.bar', 'bar'); @@ -121,6 +235,24 @@ public function testPatternPlaceholders() $routes->add('foo', new Route('/before/%parameter.foo%/after/%%escaped%%')); + $sc = $this->getPsr11ServiceContainer($routes); + $parameters = $this->getParameterBag(array('parameter.foo' => 'foo')); + + $router = new Router($sc, 'foo', array(), null, $parameters); + $route = $router->getRouteCollection()->get('foo'); + + $this->assertEquals( + '/before/foo/after/%escaped%', + $route->getPath() + ); + } + + public function testPatternPlaceholdersWithSfContainer() + { + $routes = new RouteCollection(); + + $routes->add('foo', new Route('/before/%parameter.foo%/after/%%escaped%%')); + $sc = $this->getServiceContainer($routes); $sc->setParameter('parameter.foo', 'foo'); @@ -143,6 +275,20 @@ public function testEnvPlaceholders() $routes->add('foo', new Route('/%env(FOO)%')); + $router = new Router($this->getPsr11ServiceContainer($routes), 'foo', array(), null, $this->getParameterBag()); + $router->getRouteCollection(); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage Using "%env(FOO)%" is not allowed in routing configuration. + */ + public function testEnvPlaceholdersWithSfContainer() + { + $routes = new RouteCollection(); + + $routes->add('foo', new Route('/%env(FOO)%')); + $router = new Router($this->getServiceContainer($routes), 'foo'); $router->getRouteCollection(); } @@ -156,6 +302,27 @@ public function testHostPlaceholders() $routes->add('foo', $route); + $sc = $this->getPsr11ServiceContainer($routes); + $parameters = $this->getParameterBag(array('parameter.foo' => 'foo')); + + $router = new Router($sc, 'foo', array(), null, $parameters); + $route = $router->getRouteCollection()->get('foo'); + + $this->assertEquals( + '/before/foo/after/%escaped%', + $route->getHost() + ); + } + + public function testHostPlaceholdersWithSfContainer() + { + $routes = new RouteCollection(); + + $route = new Route('foo'); + $route->setHost('/before/%parameter.foo%/after/%%escaped%%'); + + $routes->add('foo', $route); + $sc = $this->getServiceContainer($routes); $sc->setParameter('parameter.foo', 'foo'); @@ -172,7 +339,7 @@ public function testHostPlaceholders() * @expectedException \Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException * @expectedExceptionMessage You have requested a non-existent parameter "nope". */ - public function testExceptionOnNonExistentParameter() + public function testExceptionOnNonExistentParameterWithSfContainer() { $routes = new RouteCollection(); @@ -194,6 +361,23 @@ public function testExceptionOnNonStringParameter() $routes->add('foo', new Route('/%object%')); + $sc = $this->getPsr11ServiceContainer($routes); + $parameters = $this->getParameterBag(array('object' => new \stdClass())); + + $router = new Router($sc, 'foo', array(), null, $parameters); + $router->getRouteCollection()->get('foo'); + } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage The container parameter "object", used in the route configuration value "/%object%", must be a string or numeric, but it is of type object. + */ + public function testExceptionOnNonStringParameterWithSfContainer() + { + $routes = new RouteCollection(); + + $routes->add('foo', new Route('/%object%')); + $sc = $this->getServiceContainer($routes); $sc->setParameter('object', new \stdClass()); @@ -209,6 +393,23 @@ public function testDefaultValuesAsNonStrings($value) $routes = new RouteCollection(); $routes->add('foo', new Route('foo', array('foo' => $value), array('foo' => '\d+'))); + $sc = $this->getPsr11ServiceContainer($routes); + + $router = new Router($sc, 'foo', array(), null, $this->getParameterBag()); + + $route = $router->getRouteCollection()->get('foo'); + + $this->assertSame($value, $route->getDefault('foo')); + } + + /** + * @dataProvider getNonStringValues + */ + public function testDefaultValuesAsNonStringsWithSfContainer($value) + { + $routes = new RouteCollection(); + $routes->add('foo', new Route('foo', array('foo' => $value), array('foo' => '\d+'))); + $sc = $this->getServiceContainer($routes); $router = new Router($sc, 'foo'); @@ -224,6 +425,20 @@ public function testGetRouteCollectionAddsContainerParametersResource() $routeCollection->method('getIterator')->willReturn(new \ArrayIterator(array(new Route('/%locale%')))); $routeCollection->expects($this->once())->method('addResource')->with(new ContainerParametersResource(array('locale' => 'en'))); + $sc = $this->getPsr11ServiceContainer($routeCollection); + $parameters = $this->getParameterBag(array('locale' => 'en')); + + $router = new Router($sc, 'foo', array(), null, $parameters); + + $router->getRouteCollection(); + } + + public function testGetRouteCollectionAddsContainerParametersResourceWithSfContainer() + { + $routeCollection = $this->getMockBuilder(RouteCollection::class)->getMock(); + $routeCollection->method('getIterator')->willReturn(new \ArrayIterator(array(new Route('/%locale%')))); + $routeCollection->expects($this->once())->method('addResource')->with(new ContainerParametersResource(array('locale' => 'en'))); + $sc = $this->getServiceContainer($routeCollection); $sc->setParameter('locale', 'en'); @@ -260,4 +475,39 @@ private function getServiceContainer(RouteCollection $routes) return $sc; } + + private function getPsr11ServiceContainer(RouteCollection $routes): ContainerInterface + { + $loader = $this->getMockBuilder(LoaderInterface::class)->getMock(); + + $loader + ->expects($this->any()) + ->method('load') + ->will($this->returnValue($routes)) + ; + + $sc = $this->getMockBuilder(ContainerInterface::class)->getMock(); + + $sc + ->expects($this->once()) + ->method('get') + ->will($this->returnValue($loader)) + ; + + return $sc; + } + + private function getParameterBag(array $params = array()): ContainerInterface + { + $bag = $this->getMockBuilder(ContainerInterface::class)->getMock(); + $bag + ->expects($this->any()) + ->method('get') + ->will($this->returnCallback(function ($key) use ($params) { + return isset($params[$key]) ? $params[$key] : null; + })) + ; + + return $bag; + } }