From 7bf7eb8343834a5dffefb7b02ed54980dac038e7 Mon Sep 17 00:00:00 2001
From: Dries <dries@buytaert.net>
Date: Wed, 14 Nov 2012 13:51:52 -0500
Subject: [PATCH] Issue #1801570 by effulgentsia, klausi, Crell: DX: Replace
 hook_route_info() with YAML files and RouteBuildEvent.

---
 core/lib/Drupal/Core/CoreBundle.php           |  4 +-
 .../Drupal/Core/Routing/RouteBuildEvent.php   | 54 ++++++++++++++++
 core/lib/Drupal/Core/Routing/RouteBuilder.php | 52 +++++++++++++---
 .../lib/Drupal/Core/Routing/RoutingEvents.php | 38 ++++++++++++
 .../system/Tests/Routing/RouterTest.php       | 16 +++++
 core/modules/system/system.api.php            | 45 --------------
 .../router_test/RouteTestSubscriber.php       | 62 +++++++++++++++++++
 .../Drupal/router_test/RouterTestBundle.php   | 24 +++++++
 .../Drupal/router_test/TestControllers.php    |  4 ++
 .../modules/router_test/router_test.module    | 33 ----------
 .../router_test/router_test.routing.yml       | 25 ++++++++
 core/update.php                               |  3 +-
 12 files changed, 273 insertions(+), 87 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Routing/RouteBuildEvent.php
 create mode 100644 core/lib/Drupal/Core/Routing/RoutingEvents.php
 create mode 100644 core/modules/system/tests/modules/router_test/lib/Drupal/router_test/RouteTestSubscriber.php
 create mode 100644 core/modules/system/tests/modules/router_test/lib/Drupal/router_test/RouterTestBundle.php
 create mode 100644 core/modules/system/tests/modules/router_test/router_test.routing.yml

diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php
index bbc4e2e3dfeb..622ce3d818f7 100644
--- a/core/lib/Drupal/Core/CoreBundle.php
+++ b/core/lib/Drupal/Core/CoreBundle.php
@@ -72,7 +72,9 @@ public function build(ContainerBuilder $container) {
       ->addArgument(new Reference('database'));
     $container->register('router.builder', 'Drupal\Core\Routing\RouteBuilder')
       ->addArgument(new Reference('router.dumper'))
-      ->addArgument(new Reference('lock'));
+      ->addArgument(new Reference('lock'))
+      ->addArgument(new Reference('dispatcher'));
+
 
     $container->register('matcher', 'Drupal\Core\Routing\ChainMatcher');
     $container->register('legacy_url_matcher', 'Drupal\Core\LegacyUrlMatcher')
diff --git a/core/lib/Drupal/Core/Routing/RouteBuildEvent.php b/core/lib/Drupal/Core/Routing/RouteBuildEvent.php
new file mode 100644
index 000000000000..017b83a83316
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/RouteBuildEvent.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Routing\RouteBuildEvent.
+ */
+
+namespace Drupal\Core\Routing;
+
+use Symfony\Component\EventDispatcher\Event;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Represents route building information as event.
+ */
+class RouteBuildEvent extends Event {
+
+  /**
+   * The route collection.
+   *
+   * @var \Symfony\Component\Routing\RouteCollection
+   */
+  protected $routeCollection;
+
+  /**
+   * The module name that provides the route.
+   *
+   * @var string
+   */
+  protected $module;
+
+  /**
+   * Constructs a RouteBuildEvent object.
+   */
+  public function __construct(RouteCollection $route_collection, $module) {
+    $this->routeCollection = $route_collection;
+    $this->module = $module;
+  }
+
+  /**
+   * Gets the route collection.
+   */
+  public function getRouteCollection() {
+    return $this->routeCollection;
+  }
+
+  /**
+   * Gets the module that provides the route.
+   */
+  public function getModule() {
+    return $this->module;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Routing/RouteBuilder.php b/core/lib/Drupal/Core/Routing/RouteBuilder.php
index 3a27767c5683..fc12ee8f7685 100644
--- a/core/lib/Drupal/Core/Routing/RouteBuilder.php
+++ b/core/lib/Drupal/Core/Routing/RouteBuilder.php
@@ -7,8 +7,13 @@
 
 namespace Drupal\Core\Routing;
 
-use Drupal\Core\Lock\LockBackendInterface;
 use Symfony\Component\Routing\Matcher\Dumper\MatcherDumperInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\Yaml\Parser;
+use Symfony\Component\Routing\RouteCollection;
+use Symfony\Component\Routing\Route;
+
+use Drupal\Core\Lock\LockBackendInterface;
 
 /**
  * Managing class for rebuilding the router table.
@@ -32,6 +37,13 @@ class RouteBuilder {
    */
   protected $lock;
 
+  /**
+   * The event dispatcher to notify of routes.
+   *
+   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+   */
+  protected $dispatcher;
+
   /**
    * Construcs the RouteBuilder using the passed MatcherDumperInterface.
    *
@@ -39,10 +51,13 @@ class RouteBuilder {
    *   The matcher dumper used to store the route information.
    * @param \Drupal\Core\Lock\LockBackendInterface $lock
    *   The lock backend.
+   * @param \Symfony\Component\EventDispatcherEventDispatcherInterface
+   *   The event dispatcher to notify of routes.
    */
-  public function __construct(MatcherDumperInterface $dumper, LockBackendInterface $lock) {
+  public function __construct(MatcherDumperInterface $dumper, LockBackendInterface $lock, EventDispatcherInterface $dispatcher) {
     $this->dumper = $dumper;
     $this->lock = $lock;
+    $this->dispatcher = $dispatcher;
   }
 
   /**
@@ -57,15 +72,38 @@ public function rebuild() {
       return;
     }
 
+    $parser = new Parser();
+
     // We need to manually call each module so that we can know which module
     // a given item came from.
-
-    foreach (module_implements('route_info') as $module) {
-      $routes = call_user_func($module . '_route_info');
-      drupal_alter('router_info', $routes, $module);
-      $this->dumper->addRoutes($routes);
+    // @todo Use an injected Extension service rather than module_list():
+    //   http://drupal.org/node/1331486.
+    foreach (module_list() as $module) {
+      $collection = new RouteCollection();
+      $routing_file = DRUPAL_ROOT . '/' . drupal_get_path('module', $module) . '/' . $module . '.routing.yml';
+      if (file_exists($routing_file)) {
+        $routes = $parser->parse(file_get_contents($routing_file));
+        if (!empty($routes)) {
+          foreach ($routes as $name => $route_info) {
+            $defaults = isset($route_info['defaults']) ? $route_info['defaults'] : array();
+            $requirements = isset($route_info['requirements']) ? $route_info['requirements'] : array();
+            $route = new Route($route_info['pattern'], $defaults, $requirements);
+            $collection->add($name, $route);
+          }
+        }
+      }
+      $this->dispatcher->dispatch(RoutingEvents::ALTER, new RouteBuildEvent($collection, $module));
+      $this->dumper->addRoutes($collection);
       $this->dumper->dump(array('route_set' => $module));
     }
+
+    // Now allow modules to register additional, dynamic routes.
+    $collection = new RouteCollection();
+    $this->dispatcher->dispatch(RoutingEvents::DYNAMIC, new RouteBuildEvent($collection, 'dynamic_routes'));
+    $this->dispatcher->dispatch(RoutingEvents::ALTER, new RouteBuildEvent($collection, 'dynamic_routes'));
+    $this->dumper->addRoutes($collection);
+    $this->dumper->dump(array('route_set' => 'dynamic_routes'));
+
     $this->lock->release('router_rebuild');
   }
 
diff --git a/core/lib/Drupal/Core/Routing/RoutingEvents.php b/core/lib/Drupal/Core/Routing/RoutingEvents.php
new file mode 100644
index 000000000000..3ca6ef6e9fa2
--- /dev/null
+++ b/core/lib/Drupal/Core/Routing/RoutingEvents.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Definition of \Drupal\Core\Routing\RoutingEvents.
+ */
+
+namespace Drupal\Core\Routing;
+
+/**
+ * Contains all events thrown in the core routing component.
+ */
+final class RoutingEvents {
+
+  /**
+   * The ALTER event is fired on a route collection to allow changes to routes.
+   *
+   * This event is used to process new routes before they get saved.
+   *
+   * @see \Drupal\Core\Routing\RouteBuildEvent
+   *
+   * @var string
+   */
+  const ALTER = 'routing.route_alter';
+
+  /**
+   * The DYNAMIC event is fired to allow modules to register additional routes.
+   *
+   * Most routes are static, an should be defined as such. Dynamic routes are
+   * only those whose existence changes depending on the state of the system
+   * at runtime, depending on configuration.
+   *
+   * @see \Drupal\Core\Routing\RouteBuildEvent
+   *
+   * @var string
+   */
+  const DYNAMIC = 'routing.route_dynamic';
+}
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 413737516ac3..e4611354fa9f 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,20 @@ public function testControllerPlaceholdersDefaultValues() {
     $this->assertNoPattern('#</body>.*</body>#s', 'There was no double-page effect from a misrendered subrequest.');
   }
 
+  /**
+   * Checks that dynamically defined and altered routes work correctly.
+   *
+   * @see \Drupal\router_test\RouteSubscriber
+   */
+  public function testDynamicRoutes() {
+    // Test the dynamically added route.
+    $this->drupalGet('router_test/test5');
+    $this->assertResponse(200);
+    $this->assertRaw('test5', 'The correct string was returned because the route was successful.');
+
+    // Test the altered route.
+    $this->drupalGet('router_test/test6');
+    $this->assertResponse(200);
+    $this->assertRaw('test5', 'The correct string was returned because the route was successful.');
+  }
 }
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index d10d3e072a53..e9c086086e73 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -565,51 +565,6 @@ function hook_menu_get_item_alter(&$router_item, $path, $original_map) {
   }
 }
 
-/**
- * Defines routes in the new router system.
- *
- * A route is a Symfony Route object.  See the Symfony documentation for more
- * details on the available options.  Of specific note:
- *  - _controller: This is the PHP callable that will handle a request matching
- *              the route.
- *  - _content: This is the PHP callable that will handle the body of a request
- *              matching this route.  A default controller will provide the page
- *              rendering around it.
- *
- * Typically you will only specify one or the other of those properties.
- *
- * @deprecated
- *   This mechanism for registering routes is temporary. It will be replaced
- *   by a more robust mechanism in the near future.  It is documented here
- *   only for completeness.
- */
-function hook_route_info() {
-  $collection = new RouteCollection();
-
-  $route = new Route('router_test/test1', array(
-    '_controller' => '\Drupal\router_test\TestControllers::test1'
-  ));
-  $collection->add('router_test_1', $route);
-
-  $route = new Route('router_test/test2', array(
-    '_content' => '\Drupal\router_test\TestControllers::test2'
-  ));
-  $collection->add('router_test_2', $route);
-
-  $route = new Route('router_test/test3/{value}', array(
-    '_content' => '\Drupal\router_test\TestControllers::test3'
-  ));
-  $collection->add('router_test_3', $route);
-
-  $route = new Route('router_test/test4/{value}', array(
-    '_content' => '\Drupal\router_test\TestControllers::test4',
-    'value' => 'narf',
-  ));
-  $collection->add('router_test_4', $route);
-
-  return $collection;
-}
-
 /**
  * Define menu items and page callbacks.
  *
diff --git a/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/RouteTestSubscriber.php b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/RouteTestSubscriber.php
new file mode 100644
index 000000000000..f0b9bd7c3b35
--- /dev/null
+++ b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/RouteTestSubscriber.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * Definition of \Drupal\router_test\RouteTestSubscriber.
+ */
+
+namespace Drupal\router_test;
+
+use \Drupal\Core\Routing\RouteBuildEvent;
+use \Drupal\Core\Routing\RoutingEvents;
+use \Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use \Symfony\Component\Routing\Route;
+
+/**
+ * Listens to the dynamic route event and add a test route.
+ */
+class RouteTestSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Implements EventSubscriberInterface::getSubscribedEvents().
+   */
+  static function getSubscribedEvents() {
+    $events[RoutingEvents::DYNAMIC] = 'dynamicRoutes';
+    $events[RoutingEvents::ALTER] = 'alterRoutes';
+    return $events;
+  }
+
+  /**
+   * Adds a dynamic test route.
+   *
+   * @param \Drupal\Core\Routing\RouteBuildEvent $event
+   *   The route building event.
+   *
+   * @return \Symfony\Component\Routing\RouteCollection
+   *   The route collection that contains the new dynamic route.
+   */
+  public function dynamicRoutes(RouteBuildEvent $event) {
+    $collection = $event->getRouteCollection();
+    $route = new Route('/router_test/test5', array(
+      '_content' => '\Drupal\router_test\TestControllers::test5'
+    ));
+    $collection->add('router_test_5', $route);
+  }
+
+  /**
+   * Alters an existing test route.
+   *
+   * @param \Drupal\Core\Routing\RouteBuildEvent $event
+   *   The route building event.
+   *
+   * @return \Symfony\Component\Routing\RouteCollection
+   *   The altered route collection.
+   */
+  public function alterRoutes(RouteBuildEvent $event) {
+    if ($event->getModule() == 'router_test') {
+      $collection = $event->getRouteCollection();
+      $route = $collection->get('router_test_6');
+      // Change controller method from test1 to test5.
+      $route->setDefault('_controller', '\Drupal\router_test\TestControllers::test5');
+    }
+  }
+}
diff --git a/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/RouterTestBundle.php b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/RouterTestBundle.php
new file mode 100644
index 000000000000..f2e123b3d4c2
--- /dev/null
+++ b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/RouterTestBundle.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @file
+ * Definition of \Drupal\router_test\RouterTestBundle.
+ */
+
+namespace Drupal\router_test;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\HttpKernel\Bundle\Bundle;
+
+/**
+ * Registers a dynamic route provider.
+ */
+class RouterTestBundle extends Bundle {
+
+  /**
+   * Overrides Symfony\Component\HttpKernel\Bundle\Bundle::build().
+   */
+  public function build(ContainerBuilder $container) {
+    $container->register('router_test.subscriber', 'Drupal\router_test\RouteTestSubscriber')->addTag('event_subscriber');
+  }
+}
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 fa92fd89b3e5..bcf18b7b0fdd 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
@@ -30,4 +30,8 @@ public function test4($value) {
     return $value;
   }
 
+  public function test5() {
+    return "test5";
+  }
+
 }
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 4da939d70592..b3d9bbc7f371 100644
--- a/core/modules/system/tests/modules/router_test/router_test.module
+++ b/core/modules/system/tests/modules/router_test/router_test.module
@@ -1,34 +1 @@
 <?php
-
-use Symfony\Component\Routing\Route;
-use Symfony\Component\Routing\RouteCollection;
-
-/**
- * Implements hook_router_info().
- */
-function router_test_route_info() {
-  $collection = new RouteCollection();
-
-  $route = new Route('router_test/test1', array(
-    '_controller' => '\Drupal\router_test\TestControllers::test1'
-  ));
-  $collection->add('router_test_1', $route);
-
-  $route = new Route('router_test/test2', array(
-    '_content' => '\Drupal\router_test\TestControllers::test2'
-  ));
-  $collection->add('router_test_2', $route);
-
-  $route = new Route('router_test/test3/{value}', array(
-    '_content' => '\Drupal\router_test\TestControllers::test3'
-  ));
-  $collection->add('router_test_3', $route);
-
-  $route = new Route('router_test/test4/{value}', array(
-    '_content' => '\Drupal\router_test\TestControllers::test4',
-    'value' => 'narf',
-  ));
-  $collection->add('router_test_4', $route);
-
-  return $collection;
-}
diff --git a/core/modules/system/tests/modules/router_test/router_test.routing.yml b/core/modules/system/tests/modules/router_test/router_test.routing.yml
new file mode 100644
index 000000000000..cc177d38b37b
--- /dev/null
+++ b/core/modules/system/tests/modules/router_test/router_test.routing.yml
@@ -0,0 +1,25 @@
+router_test_1:
+  pattern: '/router_test/test1'
+  defaults:
+    _controller: '\Drupal\router_test\TestControllers::test1'
+
+router_test_2:
+  pattern: '/router_test/test2'
+  defaults:
+    _content: '\Drupal\router_test\TestControllers::test2'
+
+router_test_3:
+  pattern: '/router_test/test3/{value}'
+  defaults:
+    _content: '\Drupal\router_test\TestControllers::test3'
+
+router_test_4:
+  pattern: '/router_test/test4/{value}'
+  defaults:
+    _content: '\Drupal\router_test\TestControllers::test4'
+    value: 'narf'
+
+router_test_6:
+  pattern: '/router_test/test6'
+  defaults:
+    _controller: '\Drupal\router_test\TestControllers::test1'
diff --git a/core/update.php b/core/update.php
index 968e8f4e4cf6..98296bbaf81f 100644
--- a/core/update.php
+++ b/core/update.php
@@ -459,7 +459,8 @@ function update_check_requirements($skip_warnings = FALSE) {
   ->addArgument(new Reference('database'));
 $container->register('router.builder', 'Drupal\Core\Routing\RouteBuilder')
   ->addArgument(new Reference('router.dumper'))
-  ->addArgument(new Reference('lock'));
+  ->addArgument(new Reference('lock'))
+  ->addArgument(new Reference('dispatcher'));
 
 // Turn error reporting back on. From now on, only fatal errors (which are
 // not passed through the error handler) will cause a message to be printed.
-- 
GitLab