diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index 879586d..a24b28e 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -69,6 +69,9 @@ public function build(ContainerBuilder $container) { $container->register('router.builder', 'Drupal\Core\Routing\RouteBuilder') ->addArgument(new Reference('router.dumper')) ->addArgument(new Reference('lock')); + $container->register('router.generator', 'Drupal\Core\Routing\UrlGenerator') + ->addArgument(new Reference('database')) + ->addArgument(new Reference('request')); $container->register('matcher', 'Drupal\Core\Routing\ChainMatcher'); $container->register('legacy_url_matcher', 'Drupal\Core\LegacyUrlMatcher') diff --git a/core/lib/Drupal/Core/Routing/UrlGenerator.php b/core/lib/Drupal/Core/Routing/UrlGenerator.php new file mode 100644 index 0000000..061fcc5 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php @@ -0,0 +1,96 @@ +connection = $connection; + $this->context = new RequestContext(); + $this->context->fromRequest($request); + $this->table = $table; + } + + /** + * Generates a URL from the given parameters. + + * @param string $name + * The name of the route. + * @param array $parameters + * An array of parameters. + * @param boolean $absolute + * Whether to generate an absolute URL. + * + * @returns string + * The generated URL. + * + * @throws \Symfony\Component\Routing\Exception\RouteNotFoundException + */ + public function generate($name, $parameters = array(), $absolute = FALSE) { + $result = $this->connection->query('SELECT route FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name = :name', array(':name' => $name)); + $route = $result->fetchField(); + + if (empty($route)) { + throw new RouteNotFoundException(sprintf('Route "%s" does not exist.', $name)); + } + + $route = unserialize($route); + + // We cannot know if and what compiler class is set in the route coming from + // the database. To make Symfony's URL generation code work we have to set + // it here explicitly. + // @todo Reasonably combine \Symfony\Component\Routing\RouteCompiler with + // \Drupal\Core\Routing\RouteCompiler. + $route->setOption('compiler_class', '\Symfony\Component\Routing\RouteCompiler'); + + $compiledRoute = $route->compile(); + + return $this->doGenerate($compiledRoute->getVariables(), $route->getDefaults(), $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $absolute); + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php index 4137375..67d8043 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/RouterTest.php @@ -86,4 +86,22 @@ public function testControllerPlaceholdersDefaultValues() { $this->assertNoPattern('#.*#s', 'There was no double-page effect from a misrendered subrequest.'); } + /** + * Confirms the UrlGenerator registered to the container is working. + */ + public function testUrlGenerator() { + // This is only to test if the generator is actually doing anything useful. + // The unit tests to confirm that what the generator does is correct, can be + // found in /core/modules/system/lib/Drupal/system/Tests/Routing\ + // /UrlGeneratorTest.php + + // The controller behind 'router_test/test5' returns the generated URL for + // test2. As we know how it should look like, we can test if the + // UrlGenerator registered to the container is actually working. + // See the controllers in core/modules/system/tests/modules/router_test/lib\ + // /Drupal/router_test/TestControllers.php and the route definitions in + // core/modules/system/tests/modules/router_test/router_test.module. + $this->drupalGet('router_test/test5'); + $this->assertRaw('router_test/test2', 'The correct URL was returned because it was generated correctly.'); + } } diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/UrlGeneratorTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/UrlGeneratorTest.php new file mode 100644 index 0000000..25b1f02 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/UrlGeneratorTest.php @@ -0,0 +1,173 @@ + 'URL generator tests', + 'description' => 'Confirm that the URL generation code is working correctly.', + 'group' => 'Routing', + ); + } + + function __construct($test_id = NULL) { + parent::__construct($test_id); + + $this->fixtures = new RoutingFixtures(); + + // A collection of fake requests to provide good diversity of possible + // contexts URLs are generated in. Basically stolen from + // \Symfony\Component\HttpFoundation\Tests\RequestTest. + $this->requests = array( + // simple URL + Request::create('http://example.com'), + // URL with GET parameters. + Request::create('http://example.com/foo?bar=baz'), + // HTTPs URL with GET parameters + Request::create('https://example.com/foo?bar=baz'), + // URL with explicit port definition + Request::create('https://example.com:90/foo'), + // URL without protocol part + Request::create('example.com:90/foo'), + // URL with IPv4 host + Request::create('https://127.0.0.1:90/foo'), + // URL with IPv6 host + Request::create('https://[::1]:90/foo'), + // URL + POST data + Request::create('http://example.com/jsonrpc', 'POST', array(), array(), array(), array(), '{"jsonrpc":"2.0","method":"echo","id":7,"params":["Hello World"]}'), + // URL with user and password data + Request::create('http://example:test@test.com'), + ); + } + + public function tearDown() { + $this->fixtures->dropTables(Database::getConnection()); + + parent::tearDown(); + } + + /** + * Confirms correct URL generation. + * Tested against a sample RouteCollection without any parameters. + */ + function testSampleUrlGeneration() { + // Prepare database table. + $connection = Database::getConnection(); + $this->fixtures->createTables($connection); + + // Dump test collection of routes to the database. + $dumper = new MatcherDumper($connection, 'test_routes'); + $sampleRoutes = $this->fixtures->sampleRouteCollection(); + $dumper->addRoutes($sampleRoutes); + $dumper->dump(); + + // Test all routes in the collection in context of all the requests given. + foreach ($this->requests as $request) { + $generator = new UrlGenerator(Database::getConnection(), $request, "test_routes"); + + foreach ($sampleRoutes->all() as $name => $route) { + $url = $generator->generate($name); + $this->assertEqual($route->getPattern(), $url, format_string('Correct path generated for route @name.', array('@name' => $name))); + } + } + } + + /** + * Confirms correct URL generation. + * Tested against a more complex RouteCollection with parameters. + */ + function testComplexUrlGeneration() { + // Prepare database table. + $connection = Database::getConnection(); + $this->fixtures->createTables($connection); + + // Dump test collection of routes to the database. + $dumper = new MatcherDumper($connection, 'test_routes'); + $sampleRoutes = $this->fixtures->complexRouteCollection(); + $dumper->addRoutes($sampleRoutes); + $dumper->dump(); + + // Collection of sample parameters to fill into the route pattern + $sampleArgs = array('foo', 'bar', 'baz'); + + // Test all routes in the collection in context of all the requests given. + foreach ($this->requests as $request) { + $generator = new UrlGenerator(Database::getConnection(), $request, "test_routes"); + + foreach ($sampleRoutes->all() as $name => $route) { + $pattern = $route->getPattern(); + + // Construct the expected URL by pattern matching, filling in the sample + // parameters. + $expected_url = $pattern; + $parameters = array(); + preg_match_all('/\{(.+?)\}/', $pattern, $placeholders); + foreach ($placeholders[1] as $idx => $placeholder_name) { + $expected_url = preg_replace('/\{' . $placeholder_name . '\}/', $sampleArgs[$idx], $expected_url); + $parameters[$placeholder_name] = $sampleArgs[$idx]; + } + + $url = $generator->generate($name, $parameters); + $this->assertEqual($expected_url, $url, format_string('Correct path generated for route @name.', array('@name' => $name))); + } + } + } + + /** + * Confirms correct behavior for non existent routes. + */ + function testNonExistentRoute() { + // Prepare database table. + $connection = Database::getConnection(); + $this->fixtures->createTables($connection, 'test_routes'); + + // Not dumping any routes here on purpose. An empty table makes sure the + // requested route will not exist. + + $generator = new UrlGenerator($connection, $this->requests[0], "test_routes"); + + $msg = "Caught exception on generating URL for non-existent route."; + + try { + $generator->generate('surelyNonExistentName'); + $this->fail($msg); + } catch (RouteNotFoundException $e) { + $this->pass($msg); + } + } +} diff --git a/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestControllers.php b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestControllers.php index fa92fd8..5175022 100644 --- a/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestControllers.php +++ b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestControllers.php @@ -8,11 +8,12 @@ namespace Drupal\router_test; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\DependencyInjection\ContainerAware; /** * Controller routines for testing the routing system. */ -class TestControllers { +class TestControllers extends ContainerAware { public function test1() { return new Response('test1'); @@ -30,4 +31,12 @@ public function test4($value) { return $value; } + /** + * Return a generated URL for testing. + */ + public function test5() { + // This generates a URL for 'router_test_2', which is defined in + // core/modules/system/tests/modules/router_test/router_test.module. + return $this->container->get('router.generator')->generate('router_test_2'); + } } diff --git a/core/modules/system/tests/modules/router_test/router_test.module b/core/modules/system/tests/modules/router_test/router_test.module index 4da939d..69b6ded 100644 --- a/core/modules/system/tests/modules/router_test/router_test.module +++ b/core/modules/system/tests/modules/router_test/router_test.module @@ -30,5 +30,10 @@ function router_test_route_info() { )); $collection->add('router_test_4', $route); + $route = new Route('router_test/test5', array( + '_content' => '\Drupal\router_test\TestControllers::test5' + )); + $collection->add('router_test_5', $route); + return $collection; }