3 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;
12 use Drupal\Core\ParamConverter\ParamNotConvertedException;
13 use Drupal\Core\Path\CurrentPathStack;
14 use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
15 use Drupal\Core\Routing\RequestContext;
16 use Drupal\Core\Routing\RouteMatch;
17 use Drupal\Core\Routing\RouteMatchInterface;
18 use Drupal\Core\Session\AccountInterface;
19 use Drupal\Core\StringTranslation\StringTranslationTrait;
21 use Symfony\Component\HttpFoundation\Request;
22 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
23 use Symfony\Component\Routing\Exception\MethodNotAllowedException;
24 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
25 use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
28 * Class to define the menu_link breadcrumb builder.
30 class PathBasedBreadcrumbBuilder implements BreadcrumbBuilderInterface {
31 use StringTranslationTrait;
34 * The router request context.
36 * @var \Drupal\Core\Routing\RequestContext
41 * The menu link access service.
43 * @var \Drupal\Core\Access\AccessManagerInterface
45 protected $accessManager;
48 * The dynamic router service.
50 * @var \Symfony\Component\Routing\Matcher\RequestMatcherInterface
55 * The inbound path processor.
57 * @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface
59 protected $pathProcessor;
64 * @var \Drupal\Core\Config\Config
71 * @var \Drupal\Core\Controller\TitleResolverInterface
73 protected $titleResolver;
76 * The current user object.
78 * @var \Drupal\Core\Session\AccountInterface
80 protected $currentUser;
83 * Constructs the PathBasedBreadcrumbBuilder.
85 * @param \Drupal\Core\Routing\RequestContext $context
86 * The router request context.
87 * @param \Drupal\Core\Access\AccessManagerInterface $access_manager
88 * The menu link access service.
89 * @param \Symfony\Component\Routing\Matcher\RequestMatcherInterface $router
90 * The dynamic router service.
91 * @param \Drupal\Core\PathProcessor\InboundPathProcessorInterface $path_processor
92 * The inbound path processor.
93 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
94 * The config factory service.
95 * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
96 * The title resolver service.
97 * @param \Drupal\Core\Session\AccountInterface $current_user
98 * The current user object.
99 * @param \Drupal\Core\Path\CurrentPathStack $current_path
102 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) {
103 $this->context = $context;
104 $this->accessManager = $access_manager;
105 $this->router = $router;
106 $this->pathProcessor = $path_processor;
107 $this->config = $config_factory->get('system.site');
108 $this->titleResolver = $title_resolver;
109 $this->currentUser = $current_user;
110 $this->currentPath = $current_path;
116 public function applies(RouteMatchInterface $route_match) {
123 public function build(RouteMatchInterface $route_match) {
124 $breadcrumb = new Breadcrumb();
127 // General path-based breadcrumbs. Use the actual request path, prior to
128 // resolving path aliases, so the breadcrumb can be defined by simply
129 // creating a hierarchy of path aliases.
130 $path = trim($this->context->getPathInfo(), '/');
131 $path_elements = explode('/', $path);
133 // Don't show a link to the front-page path.
134 $front = $this->config->get('page.front');
135 $exclude[$front] = TRUE;
136 // /user is just a redirect, so skip it.
137 // @todo Find a better way to deal with /user.
138 $exclude['/user'] = TRUE;
139 // Add the url.path.parent cache context. This code ignores the last path
140 // part so the result only depends on the path parents.
141 $breadcrumb->addCacheContexts(['url.path.parent']);
142 while (count($path_elements) > 1) {
143 array_pop($path_elements);
144 // Copy the path elements for up-casting.
145 $route_request = $this->getRequestForPath('/' . implode('/', $path_elements), $exclude);
146 if ($route_request) {
147 $route_match = RouteMatch::createFromRequest($route_request);
148 $access = $this->accessManager->check($route_match, $this->currentUser, NULL, TRUE);
149 // The set of breadcrumb links depends on the access result, so merge
150 // the access result's cacheability metadata.
151 $breadcrumb = $breadcrumb->addCacheableDependency($access);
152 if ($access->isAllowed()) {
153 $title = $this->titleResolver->getTitle($route_request, $route_match->getRouteObject());
154 if (!isset($title)) {
155 // Fallback to using the raw path component as the title if the
156 // route is missing a _title or _title_callback attribute.
157 $title = str_replace(['-', '_'], ' ', Unicode::ucfirst(end($path_elements)));
159 $url = Url::fromRouteMatch($route_match);
160 $links[] = new Link($title, $url);
165 if ($path && '/' . $path != $front) {
166 // Add the Home link, except for the front page.
167 $links[] = Link::createFromRoute($this->t('Home'), '<front>');
170 return $breadcrumb->setLinks(array_reverse($links));
174 * Matches a path in the router.
176 * @param string $path
177 * The request path with a leading slash.
178 * @param array $exclude
179 * An array of paths or system paths to skip.
181 * @return \Symfony\Component\HttpFoundation\Request
182 * A populated request object or NULL if the path couldn't be matched.
184 protected function getRequestForPath($path, array $exclude) {
185 if (!empty($exclude[$path])) {
188 // @todo Use the RequestHelper once https://www.drupal.org/node/2090293 is
190 $request = Request::create($path);
191 // Performance optimization: set a short accept header to reduce overhead in
192 // AcceptHeaderMatcher when matching the request.
193 $request->headers->set('Accept', 'text/html');
194 // Find the system path by resolving aliases, language prefix, etc.
195 $processed = $this->pathProcessor->processInbound($path, $request);
196 if (empty($processed) || !empty($exclude[$processed])) {
197 // This resolves to the front page, which we already add.
200 $this->currentPath->setPath($processed, $request);
201 // Attempt to match this path to provide a fully built request.
203 $request->attributes->add($this->router->matchRequest($request));
206 catch (ParamNotConvertedException $e) {
209 catch (ResourceNotFoundException $e) {
212 catch (MethodNotAllowedException $e) {
215 catch (AccessDeniedHttpException $e) {