--- /dev/null
+<?php
+
+namespace Drupal\system;
+
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Access\AccessManagerInterface;
+use Drupal\Core\Breadcrumb\Breadcrumb;
+use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Controller\TitleResolverInterface;
+use Drupal\Core\Link;
+use Drupal\Core\ParamConverter\ParamNotConvertedException;
+use Drupal\Core\Path\CurrentPathStack;
+use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
+use Drupal\Core\Routing\RequestContext;
+use Drupal\Core\Routing\RouteMatch;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Url;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+use Symfony\Component\Routing\Exception\MethodNotAllowedException;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
+
+/**
+ * Class to define the menu_link breadcrumb builder.
+ */
+class PathBasedBreadcrumbBuilder implements BreadcrumbBuilderInterface {
+ use StringTranslationTrait;
+
+ /**
+ * The router request context.
+ *
+ * @var \Drupal\Core\Routing\RequestContext
+ */
+ protected $context;
+
+ /**
+ * The menu link access service.
+ *
+ * @var \Drupal\Core\Access\AccessManagerInterface
+ */
+ protected $accessManager;
+
+ /**
+ * The dynamic router service.
+ *
+ * @var \Symfony\Component\Routing\Matcher\RequestMatcherInterface
+ */
+ protected $router;
+
+ /**
+ * The inbound path processor.
+ *
+ * @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;
+
+ /**
+ * The current user object.
+ *
+ * @var \Drupal\Core\Session\AccountInterface
+ */
+ protected $currentUser;
+
+ /**
+ * Constructs the PathBasedBreadcrumbBuilder.
+ *
+ * @param \Drupal\Core\Routing\RequestContext $context
+ * The router request context.
+ * @param \Drupal\Core\Access\AccessManagerInterface $access_manager
+ * 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.
+ * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+ * The config factory service.
+ * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
+ * The title resolver service.
+ * @param \Drupal\Core\Session\AccountInterface $current_user
+ * The current user object.
+ * @param \Drupal\Core\Path\CurrentPathStack $current_path
+ * The current path.
+ */
+ 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) {
+ $this->context = $context;
+ $this->accessManager = $access_manager;
+ $this->router = $router;
+ $this->pathProcessor = $path_processor;
+ $this->config = $config_factory->get('system.site');
+ $this->titleResolver = $title_resolver;
+ $this->currentUser = $current_user;
+ $this->currentPath = $current_path;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function applies(RouteMatchInterface $route_match) {
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(RouteMatchInterface $route_match) {
+ $breadcrumb = new Breadcrumb();
+ $links = [];
+
+ // 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.
+ $path = trim($this->context->getPathInfo(), '/');
+ $path_elements = explode('/', $path);
+ $exclude = [];
+ // 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.
+ $exclude['/user'] = TRUE;
+ // Add the url.path.parent cache context. This code ignores the last path
+ // part so the result only depends on the path parents.
+ $breadcrumb->addCacheContexts(['url.path.parent']);
+ while (count($path_elements) > 1) {
+ array_pop($path_elements);
+ // Copy the path elements for up-casting.
+ $route_request = $this->getRequestForPath('/' . implode('/', $path_elements), $exclude);
+ if ($route_request) {
+ $route_match = RouteMatch::createFromRequest($route_request);
+ $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()) {
+ $title = $this->titleResolver->getTitle($route_request, $route_match->getRouteObject());
+ if (!isset($title)) {
+ // Fallback to using the raw path component as the title if the
+ // route is missing a _title or _title_callback attribute.
+ $title = str_replace(['-', '_'], ' ', Unicode::ucfirst(end($path_elements)));
+ }
+ $url = Url::fromRouteMatch($route_match);
+ $links[] = new Link($title, $url);
+ }
+ }
+
+ }
+ if ($path && '/' . $path != $front) {
+ // Add the Home link, except for the front page.
+ $links[] = Link::createFromRoute($this->t('Home'), '<front>');
+ }
+
+ return $breadcrumb->setLinks(array_reverse($links));
+ }
+
+ /**
+ * Matches a path in the router.
+ *
+ * @param string $path
+ * The request path with a leading slash.
+ * @param array $exclude
+ * An array of paths or system paths to skip.
+ *
+ * @return \Symfony\Component\HttpFoundation\Request
+ * A populated request object or NULL if the path couldn't be matched.
+ */
+ protected function getRequestForPath($path, array $exclude) {
+ if (!empty($exclude[$path])) {
+ return NULL;
+ }
+ // @todo Use the RequestHelper once https://www.drupal.org/node/2090293 is
+ // fixed.
+ $request = Request::create($path);
+ // Performance optimization: set a short accept header to reduce overhead in
+ // AcceptHeaderMatcher when matching the request.
+ $request->headers->set('Accept', 'text/html');
+ // 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;
+ }
+ $this->currentPath->setPath($processed, $request);
+ // Attempt to match this path to provide a fully built request.
+ try {
+ $request->attributes->add($this->router->matchRequest($request));
+ return $request;
+ }
+ catch (ParamNotConvertedException $e) {
+ return NULL;
+ }
+ catch (ResourceNotFoundException $e) {
+ return NULL;
+ }
+ catch (MethodNotAllowedException $e) {
+ return NULL;
+ }
+ catch (AccessDeniedHttpException $e) {
+ return NULL;
+ }
+ }
+
+}