PathBasedBreadcrumbBuilder.php 7.93 KB
Newer Older
1 2 3 4
<?php

namespace Drupal\system;

5
use Drupal\Component\Utility\Unicode;
6
use Drupal\Core\Access\AccessManagerInterface;
7
use Drupal\Core\Breadcrumb\Breadcrumb;
8
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
9
use Drupal\Core\Config\ConfigFactoryInterface;
10
use Drupal\Core\Controller\TitleResolverInterface;
11
use Drupal\Core\Link;
12
use Drupal\Core\ParamConverter\ParamNotConvertedException;
13
use Drupal\Core\Path\CurrentPathStack;
14
use Drupal\Core\Path\PathMatcherInterface;
15
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
16
use Drupal\Core\Routing\RequestContext;
17
use Drupal\Core\Routing\RouteMatch;
18
use Drupal\Core\Routing\RouteMatchInterface;
19
use Drupal\Core\Session\AccountInterface;
20
use Drupal\Core\StringTranslation\StringTranslationTrait;
21
use Drupal\Core\Url;
22
use Symfony\Component\HttpFoundation\Request;
23
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
24
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
25
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
26
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
27 28 29 30

/**
 * Class to define the menu_link breadcrumb builder.
 */
31 32
class PathBasedBreadcrumbBuilder implements BreadcrumbBuilderInterface {
  use StringTranslationTrait;
33 34

  /**
35
   * The router request context.
36
   *
37
   * @var \Drupal\Core\Routing\RequestContext
38
   */
39
  protected $context;
40 41 42 43

  /**
   * The menu link access service.
   *
44
   * @var \Drupal\Core\Access\AccessManagerInterface
45 46 47 48 49 50 51 52 53 54 55
   */
  protected $accessManager;

  /**
   * The dynamic router service.
   *
   * @var \Symfony\Component\Routing\Matcher\RequestMatcherInterface
   */
  protected $router;

  /**
56
   * The inbound path processor.
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
   *
   * @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface
   */
  protected $pathProcessor;

  /**
   * Site config object.
   *
   * @var \Drupal\Core\Config\Config
   */
  protected $config;

  /**
   * The title resolver.
   *
   * @var \Drupal\Core\Controller\TitleResolverInterface
   */
  protected $titleResolver;

76 77 78 79 80 81 82
  /**
   * The current user object.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

83 84 85 86 87 88 89 90 91 92 93 94 95 96
  /**
   * The current path service.
   *
   * @var \Drupal\Core\Path\CurrentPathStack
   */
  protected $currentPath;

  /**
   * The patch matcher service.
   *
   * @var \Drupal\Core\Path\PathMatcherInterface
   */
  protected $pathMatcher;

97 98 99
  /**
   * Constructs the PathBasedBreadcrumbBuilder.
   *
100
   * @param \Drupal\Core\Routing\RequestContext $context
101
   *   The router request context.
102
   * @param \Drupal\Core\Access\AccessManagerInterface $access_manager
103 104 105 106 107
   *   The menu link access service.
   * @param \Symfony\Component\Routing\Matcher\RequestMatcherInterface $router
   *   The dynamic router service.
   * @param \Drupal\Core\PathProcessor\InboundPathProcessorInterface $path_processor
   *   The inbound path processor.
108
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
109 110 111
   *   The config factory service.
   * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
   *   The title resolver service.
112 113
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user object.
114 115
   * @param \Drupal\Core\Path\CurrentPathStack $current_path
   *   The current path.
116 117
   * @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
   *   The path matcher service.
118
   */
119
  public function __construct(RequestContext $context, AccessManagerInterface $access_manager, RequestMatcherInterface $router, InboundPathProcessorInterface $path_processor, ConfigFactoryInterface $config_factory, TitleResolverInterface $title_resolver, AccountInterface $current_user, CurrentPathStack $current_path, PathMatcherInterface $path_matcher = NULL) {
120
    $this->context = $context;
121 122 123 124 125
    $this->accessManager = $access_manager;
    $this->router = $router;
    $this->pathProcessor = $path_processor;
    $this->config = $config_factory->get('system.site');
    $this->titleResolver = $title_resolver;
126
    $this->currentUser = $current_user;
127
    $this->currentPath = $current_path;
128
    $this->pathMatcher = $path_matcher ?: \Drupal::service('path.matcher');
129 130
  }

131 132 133
  /**
   * {@inheritdoc}
   */
134
  public function applies(RouteMatchInterface $route_match) {
135 136 137
    return TRUE;
  }

138 139 140
  /**
   * {@inheritdoc}
   */
141
  public function build(RouteMatchInterface $route_match) {
142
    $breadcrumb = new Breadcrumb();
143
    $links = [];
144

145 146
    // Add the url.path.parent cache context. This code ignores the last path
    // part so the result only depends on the path parents.
147
    $breadcrumb->addCacheContexts(['url.path.parent', 'url.path.is_front']);
148 149 150 151 152 153

    // Do not display a breadcrumb on the frontpage.
    if ($this->pathMatcher->isFrontPage()) {
      return $breadcrumb;
    }

154 155 156
    // General path-based breadcrumbs. Use the actual request path, prior to
    // resolving path aliases, so the breadcrumb can be defined by simply
    // creating a hierarchy of path aliases.
157
    $path = trim($this->context->getPathInfo(), '/');
158
    $path_elements = explode('/', $path);
159
    $exclude = [];
160 161 162 163 164
    // Don't show a link to the front-page path.
    $front = $this->config->get('page.front');
    $exclude[$front] = TRUE;
    // /user is just a redirect, so skip it.
    // @todo Find a better way to deal with /user.
165
    $exclude['/user'] = TRUE;
166 167 168
    while (count($path_elements) > 1) {
      array_pop($path_elements);
      // Copy the path elements for up-casting.
169
      $route_request = $this->getRequestForPath('/' . implode('/', $path_elements), $exclude);
170
      if ($route_request) {
171
        $route_match = RouteMatch::createFromRequest($route_request);
172 173 174 175 176
        $access = $this->accessManager->check($route_match, $this->currentUser, NULL, TRUE);
        // The set of breadcrumb links depends on the access result, so merge
        // the access result's cacheability metadata.
        $breadcrumb = $breadcrumb->addCacheableDependency($access);
        if ($access->isAllowed()) {
177
          $title = $this->titleResolver->getTitle($route_request, $route_match->getRouteObject());
178
          if (!isset($title)) {
179 180
            // Fallback to using the raw path component as the title if the
            // route is missing a _title or _title_callback attribute.
181
            $title = str_replace(['-', '_'], ' ', Unicode::ucfirst(end($path_elements)));
182
          }
183 184
          $url = Url::fromRouteMatch($route_match);
          $links[] = new Link($title, $url);
185 186 187
        }
      }
    }
188

189 190 191
    // Add the Home link.
    $links[] = Link::createFromRoute($this->t('Home'), '<front>');

192
    return $breadcrumb->setLinks(array_reverse($links));
193 194 195 196 197 198
  }

  /**
   * Matches a path in the router.
   *
   * @param string $path
199
   *   The request path with a leading slash.
200 201 202 203
   * @param array $exclude
   *   An array of paths or system paths to skip.
   *
   * @return \Symfony\Component\HttpFoundation\Request
204
   *   A populated request object or NULL if the path couldn't be matched.
205 206 207 208 209
   */
  protected function getRequestForPath($path, array $exclude) {
    if (!empty($exclude[$path])) {
      return NULL;
    }
210
    $request = Request::create($path);
211 212 213
    // Performance optimization: set a short accept header to reduce overhead in
    // AcceptHeaderMatcher when matching the request.
    $request->headers->set('Accept', 'text/html');
214 215 216 217 218 219
    // Find the system path by resolving aliases, language prefix, etc.
    $processed = $this->pathProcessor->processInbound($path, $request);
    if (empty($processed) || !empty($exclude[$processed])) {
      // This resolves to the front page, which we already add.
      return NULL;
    }
220
    $this->currentPath->setPath($processed, $request);
221 222 223 224 225
    // Attempt to match this path to provide a fully built request.
    try {
      $request->attributes->add($this->router->matchRequest($request));
      return $request;
    }
226
    catch (ParamNotConvertedException $e) {
227 228 229 230 231
      return NULL;
    }
    catch (ResourceNotFoundException $e) {
      return NULL;
    }
232 233 234
    catch (MethodNotAllowedException $e) {
      return NULL;
    }
235 236 237
    catch (AccessDeniedHttpException $e) {
      return NULL;
    }
238 239 240
  }

}