RouteBuilder.php 7.38 KB
Newer Older
1 2 3 4
<?php

namespace Drupal\Core\Routing;

5
use Drupal\Core\Access\CheckProviderInterface;
6
use Drupal\Core\Controller\ControllerResolverInterface;
7
use Drupal\Core\Discovery\YamlDiscovery;
8 9
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Lock\LockBackendInterface;
10
use Drupal\Core\DestructableInterface;
11
use Symfony\Component\EventDispatcher\Event;
12 13 14 15
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

16 17 18
/**
 * Managing class for rebuilding the router table.
 */
19
class RouteBuilder implements RouteBuilderInterface, DestructableInterface {
20

21 22 23
  /**
   * The dumper to which we should send collected routes.
   *
24
   * @var \Drupal\Core\Routing\MatcherDumperInterface
25
   */
26 27
  protected $dumper;

28 29 30
  /**
   * The used lock backend instance.
   *
31
   * @var \Drupal\Core\Lock\LockBackendInterface
32 33 34
   */
  protected $lock;

35 36 37 38 39 40 41
  /**
   * The event dispatcher to notify of routes.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  protected $dispatcher;

42 43
  /**
   * The module handler.
44 45 46 47 48
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

49 50 51 52 53 54 55
  /**
   * The controller resolver.
   *
   * @var \Drupal\Core\Controller\ControllerResolverInterface
   */
  protected $controllerResolver;

56 57 58 59 60 61 62
  /**
   * The route collection during the rebuild.
   *
   * @var \Symfony\Component\Routing\RouteCollection
   */
  protected $routeCollection;

63
  /**
64
   * Flag that indicates if we are currently rebuilding the routes.
65 66 67 68 69
   *
   * @var bool
   */
  protected $building = FALSE;

70 71 72 73 74 75 76
  /**
   * Flag that indicates if we should rebuild at the end of the request.
   *
   * @var bool
   */
  protected $rebuildNeeded = FALSE;

77 78 79 80 81 82 83
  /**
   * The check provider.
   *
   * @var \Drupal\Core\Access\CheckProviderInterface
   */
  protected $checkProvider;

84
  /**
85
   * Constructs the RouteBuilder using the passed MatcherDumperInterface.
86
   *
87
   * @param \Drupal\Core\Routing\MatcherDumperInterface $dumper
88
   *   The matcher dumper used to store the route information.
89 90
   * @param \Drupal\Core\Lock\LockBackendInterface $lock
   *   The lock backend.
91
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
92
   *   The event dispatcher to notify of routes.
93 94
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
95 96
   * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
   *   The controller resolver.
97 98
   * @param \Drupal\Core\Access\CheckProviderInterface $check_provider
   *   The check provider.
99
   */
100
  public function __construct(MatcherDumperInterface $dumper, LockBackendInterface $lock, EventDispatcherInterface $dispatcher, ModuleHandlerInterface $module_handler, ControllerResolverInterface $controller_resolver, CheckProviderInterface $check_provider) {
101
    $this->dumper = $dumper;
102
    $this->lock = $lock;
103
    $this->dispatcher = $dispatcher;
104
    $this->moduleHandler = $module_handler;
105
    $this->controllerResolver = $controller_resolver;
106
    $this->checkProvider = $check_provider;
107 108
  }

109 110 111 112 113 114 115
  /**
   * {@inheritdoc}
   */
  public function setRebuildNeeded() {
    $this->rebuildNeeded = TRUE;
  }

116
  /**
117
   * {@inheritdoc}
118
   */
119
  public function rebuild() {
120 121 122 123
    if ($this->building) {
      throw new \RuntimeException('Recursive router rebuild detected.');
    }

124 125 126 127 128
    if (!$this->lock->acquire('router_rebuild')) {
      // Wait for another request that is already doing this work.
      // We choose to block here since otherwise the routes might not be
      // available, resulting in a 404.
      $this->lock->wait('router_rebuild');
129
      return FALSE;
130 131
    }

132 133
    $this->building = TRUE;

134 135
    $collection = new RouteCollection();
    foreach ($this->getRouteDefinitions() as $routes) {
136 137 138
      // The top-level 'routes_callback' is a list of methods in controller
      // syntax, see \Drupal\Core\Controller\ControllerResolver. These methods
      // should return a set of \Symfony\Component\Routing\Route objects, either
139 140 141 142
      // in an associative array keyed by the route name, which will be iterated
      // over and added to the collection for this provider, or as a new
      // \Symfony\Component\Routing\RouteCollection object, which will be added
      // to the collection.
143 144 145 146
      if (isset($routes['route_callbacks'])) {
        foreach ($routes['route_callbacks'] as $route_callback) {
          $callback = $this->controllerResolver->getControllerFromDefinition($route_callback);
          if ($callback_routes = call_user_func($callback)) {
147 148 149 150 151 152 153 154 155
            // If a RouteCollection is returned, add the whole collection.
            if ($callback_routes instanceof RouteCollection) {
              $collection->addCollection($callback_routes);
            }
            // Otherwise, add each Route object individually.
            else {
              foreach ($callback_routes as $name => $callback_route) {
                $collection->add($name, $callback_route);
              }
156 157 158 159 160
            }
          }
        }
        unset($routes['route_callbacks']);
      }
161
      foreach ($routes as $name => $route_info) {
162 163 164 165
        $route_info += [
          'defaults' => [],
          'requirements' => [],
          'options' => [],
166
          'host' => NULL,
167 168
          'schemes' => [],
          'methods' => [],
169
          'condition' => '',
170
        ];
171 172 173 174 175
        // Ensure routes default to using Drupal's route compiler instead of
        // Symfony's.
        $route_info['options'] += [
          'compiler_class' => RouteCompiler::class,
        ];
176

177
        $route = new Route($route_info['path'], $route_info['defaults'], $route_info['requirements'], $route_info['options'], $route_info['host'], $route_info['schemes'], $route_info['methods'], $route_info['condition']);
178
        $collection->add($name, $route);
179
      }
180
    }
181

182 183 184 185 186 187 188 189 190
    // DYNAMIC is supposed to be used to add new routes based upon all the
    // static defined ones.
    $this->dispatcher->dispatch(RoutingEvents::DYNAMIC, new RouteBuildEvent($collection));

    // ALTER is the final step to alter all the existing routes. We cannot stop
    // people from adding new routes here, but we define two separate steps to
    // make it clear.
    $this->dispatcher->dispatch(RoutingEvents::ALTER, new RouteBuildEvent($collection));

191 192
    $this->checkProvider->setChecks($collection);

193
    $this->dumper->addRoutes($collection);
194
    $this->dumper->dump();
195

196
    $this->lock->release('router_rebuild');
197
    $this->dispatcher->dispatch(RoutingEvents::FINISHED, new Event());
198
    $this->building = FALSE;
199

200
    $this->rebuildNeeded = FALSE;
201

202 203 204
    return TRUE;
  }

205 206 207 208
  /**
   * {@inheritdoc}
   */
  public function rebuildIfNeeded() {
209
    if ($this->rebuildNeeded) {
210 211 212 213 214 215 216 217
      return $this->rebuild();
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
218 219 220 221 222
  public function destruct() {
    // Rebuild routes only once at the end of the request lifecycle to not
    // trigger multiple rebuilds and also make the page more responsive for the
    // user.
    $this->rebuildIfNeeded();
223 224
  }

225
  /**
226
   * Retrieves all defined routes from .routing.yml files.
227
   *
228 229
   * @return array
   *   The defined routes, keyed by provider.
230
   */
231
  protected function getRouteDefinitions() {
232 233 234 235
    // Always instantiate a new YamlDiscovery object so that we always search on
    // the up-to-date list of modules.
    $discovery = new YamlDiscovery('routing', $this->moduleHandler->getModuleDirectories());
    return $discovery->findAll();
236 237 238
  }

}