Commit 4d08d574 authored by alexpott's avatar alexpott

Issue #1947536 by andypost, donquixote, larowlan, splatio, kim.pepper,...

Issue #1947536 by andypost, donquixote, larowlan, splatio, kim.pepper, alexpott, Crell, tim.plunkett: Convert drupal_get_breadcrumb() and drupal_set_breadcrumb() to a service.
parent c4b66585
......@@ -441,6 +441,8 @@ services:
class: Drupal\system\Plugin\ImageToolkitInterface
factory_method: getDefaultToolkit
factory_service: image.toolkit.manager
breadcrumb:
class: Drupal\Core\Breadcrumb\BreadcrumbManager
token:
class: Drupal\Core\Utility\Token
arguments: ['@module_handler']
......
......@@ -288,6 +288,11 @@ function drupal_get_profile() {
* @param $breadcrumb
* Array of links, starting with "home" and proceeding up to but not including
* the current page.
*
* @deprecated This will be removed in 8.0. Instead, register a new breadcrumb
* builder service.
*
* @see Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface
*/
function drupal_set_breadcrumb($breadcrumb = NULL) {
$stored_breadcrumb = &drupal_static(__FUNCTION__);
......@@ -298,19 +303,6 @@ function drupal_set_breadcrumb($breadcrumb = NULL) {
return $stored_breadcrumb;
}
/**
* Gets the breadcrumb trail for the current page.
*/
function drupal_get_breadcrumb() {
$breadcrumb = drupal_set_breadcrumb();
if (!isset($breadcrumb)) {
$breadcrumb = menu_get_active_breadcrumb();
}
return $breadcrumb;
}
/**
* Adds output to the HEAD tag of the HTML page.
*
......
......@@ -2840,7 +2840,7 @@ function template_process_page(&$variables) {
// @see menu_tree_page_data()
$variables['breadcrumb'] = array(
'#theme' => 'breadcrumb',
'#breadcrumb' => drupal_get_breadcrumb(),
'#breadcrumb' => \Drupal::service('breadcrumb')->build(\Drupal::service('request')->attributes->all()),
);
}
if (!isset($variables['title'])) {
......
<?php
/**
* @file
* Contains \Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface.
*/
namespace Drupal\Core\Breadcrumb;
/**
* Defines an interface for classes that build breadcrumbs.
*/
interface BreadcrumbBuilderInterface {
/**
* Builds the breadcrumb.
*
* @param array $attributes
* Attributes representing the current page.
*
* @return array|null
* A render array for the breadcrumbs or NULL to let other builders decide.
* Returning empty array will suppress all breadcrumbs.
*/
public function build(array $attributes);
}
<?php
/**
* @file
* Contains \Drupal\Core\Breadcrumb\BreadcrumbManager.
*/
namespace Drupal\Core\Breadcrumb;
/**
* Provides a breadcrumb manager.
*
* Holds an array of path processor objects and uses them to sequentially process
* a path, in order of processor priority.
*/
class BreadcrumbManager implements BreadcrumbBuilderInterface {
/**
* Holds arrays of breadcrumb builders, keyed by priority.
*
* @var array
*/
protected $builders = array();
/**
* Holds the array of breadcrumb builders sorted by priority.
*
* Set to NULL if the array needs to be re-calculated.
*
* @var array|NULL
*/
protected $sortedBuilders;
/**
* Adds another breadcrumb builder.
*
* @param \Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface $builder
* The breadcrumb builder to add.
* @param int $priority
* Priority of the breadcrumb builder.
*/
public function addBuilder(BreadcrumbBuilderInterface $builder, $priority) {
$this->builders[$priority][] = $builder;
// Force the builders to be re-sorted.
$this->sortedBuilders = NULL;
}
/**
* {@inheritdoc}
*/
public function build(array $attributes) {
// Call the build method of registered breadcrumb builders,
// until one of them returns an array.
foreach ($this->getSortedBuilders() as $builder) {
$breadcrumb = $builder->build($attributes);
if (!isset($breadcrumb)) {
// The builder returned NULL, so we continue with the other builders.
continue;
}
elseif (is_array($breadcrumb)) {
// The builder returned an array of breadcrumb links.
return $breadcrumb;
}
else {
throw new \UnexpectedValueException(format_string('Invalid breadcrumb returned by !class::build().', array('!class' => get_class($builder))));
}
}
// Fall back to an empty breadcrumb.
return array();
}
/**
* Returns the sorted array of breadcrumb builders.
*
* @return array
* An array of breadcrumb builder objects.
*/
protected function getSortedBuilders() {
if (!isset($this->sortedBuilders)) {
// Sort the builders according to priority.
krsort($this->builders);
// Merge nested builders from $this->builders into $this->sortedBuilders.
$this->sortedBuilders = array();
foreach ($this->builders as $builders) {
$this->sortedBuilders = array_merge($this->sortedBuilders, $builders);
}
}
return $this->sortedBuilders;
}
}
......@@ -17,6 +17,7 @@
use Drupal\Core\DependencyInjection\Compiler\RegisterParamConvertersPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterServicesForDestructionPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterStringTranslatorsPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterBreadcrumbBuilderPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference;
......@@ -63,6 +64,9 @@ public function build(ContainerBuilder $container) {
$container->addCompilerPass(new ListCacheBinsPass());
// Add the compiler pass for appending string translators.
$container->addCompilerPass(new RegisterStringTranslatorsPass());
// Add the compiler pass that will process the tagged breadcrumb builder
// services.
$container->addCompilerPass(new RegisterBreadcrumbBuilderPass());
}
/**
......
<?php
/**
* @file
* Contains \Drupal\Core\DependencyInjection\Compiler\RegisterBreadcrumbBuilderPass.
*/
namespace Drupal\Core\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
/**
* Adds services to the breadcrumb_builder service.
*/
class RegisterBreadcrumbBuilderPass implements CompilerPassInterface {
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container) {
if (!$container->hasDefinition('breadcrumb')) {
return;
}
$manager = $container->getDefinition('breadcrumb');
foreach ($container->findTaggedServiceIds('breadcrumb_builder') as $id => $attributes) {
$priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
$manager->addMethodCall('addBuilder', array(new Reference($id), $priority));
}
}
}
......@@ -261,29 +261,6 @@ function _forum_node_check_node_type(EntityInterface $node) {
return !empty($instance);
}
/**
* Implements hook_node_view().
*/
function forum_node_view(EntityInterface $node, EntityDisplay $display, $view_mode) {
$vid = config('forum.settings')->get('vocabulary');
$vocabulary = taxonomy_vocabulary_load($vid);
if (_forum_node_check_node_type($node)) {
if ($view_mode == 'full' && node_is_page($node)) {
// Breadcrumb navigation
$breadcrumb[] = l(t('Home'), NULL);
$breadcrumb[] = l($vocabulary->name, 'forum');
if ($parents = taxonomy_term_load_parents_all($node->forum_tid)) {
$parents = array_reverse($parents);
foreach ($parents as $parent) {
$breadcrumb[] = l($parent->label(), 'forum/' . $parent->id());
}
}
drupal_set_breadcrumb($breadcrumb);
}
}
}
/**
* Implements hook_node_validate().
*
......
......@@ -28,22 +28,6 @@ function forum_page($forum_term = NULL) {
drupal_set_title($vocabulary->label());
}
// Breadcrumb navigation.
$breadcrumb[] = l(t('Home'), NULL);
if ($forum_term->id()) {
// Parent of all forums is the vocabulary name.
$breadcrumb[] = l($vocabulary->label(), 'forum');
}
// Add all parent forums to breadcrumbs.
if ($forum_term->parents) {
foreach (array_reverse($forum_term->parents) as $parent) {
if ($parent->id() != $forum_term->id()) {
$breadcrumb[] = l($parent->label(), 'forum/' . $parent->id());
}
}
}
drupal_set_breadcrumb($breadcrumb);
if ($forum_term->id() && array_search($forum_term->id(), $config->get('containers')) === FALSE) {
// Add RSS feed for forums.
drupal_add_feed('taxonomy/term/' . $forum_term->id() . '/feed', 'RSS - ' . $forum_term->label());
......
services:
forum.breadcrumb:
class: Drupal\forum\ForumBreadcrumbBuilder
arguments: ['@plugin.manager.entity', '@config.factory']
tags:
- { name: breadcrumb_builder, priority: 1001 }
<?php
/**
* @file
* Contains \Drupal\forum\ForumBreadcrumbBuilder.
*/
namespace Drupal\forum;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Config\ConfigFactory;
use Drupal\Core\Entity\EntityManager;
/**
* Class to define the forum breadcrumb builder.
*/
class ForumBreadcrumbBuilder implements BreadcrumbBuilderInterface {
/**
* Configuration object for this builder.
*
* @var \Drupal\Core\Config\Config
*/
protected $config;
/**
* Stores the Entity manager.
*
* @var \Drupal\Core\Entity\EntityManager
*/
protected $entityManager;
/**
* Constructs a new ForumBreadcrumbBuilder.
*
* @param \Drupal\Core\Entity\EntityManager
* The entity manager.
* @param \Drupal\Core\Config\ConfigFactory $configFactory
* The configuration factory.
*/
public function __construct(EntityManager $entity_manager, ConfigFactory $configFactory) {
$this->entityManager = $entity_manager;
$this->config = $configFactory->get('forum.settings');
}
/**
* {@inheritdoc}
*/
public function build(array $attributes) {
// @todo This only works for legacy routes. Once node/% and forum/% are
// converted to the new router this code will need to be updated.
if (isset($attributes['drupal_menu_item'])) {
$item = $attributes['drupal_menu_item'];
switch ($item['path']) {
case 'node/%':
$node = $item['map'][1];
// Load the object in case of missing wildcard loaders.
$node = is_object($node) ? $node : node_load($node);
if (_forum_node_check_node_type($node)) {
$breadcrumb = $this->forumPostBreadcrumb($node);
}
break;
case 'forum/%':
$term = $item['map'][1];
// Load the object in case of missing wildcard loaders.
$term = is_object($term) ? $term : forum_forum_load($term);
$breadcrumb = $this->forumTermBreadcrumb($term);
break;
}
}
if (!empty($breadcrumb)) {
return $breadcrumb;
}
}
/**
* Builds the breadcrumb for a forum post page.
*/
protected function forumPostBreadcrumb($node) {
$vocabularies = $this->entityManager->getStorageController('taxonomy_vocabulary')->load(array($this->config->get('vocabulary')));
$vocabulary = reset($vocabularies);
$breadcrumb[] = l(t('Home'), NULL);
$breadcrumb[] = l($vocabulary->label(), 'forum');
if ($parents = taxonomy_term_load_parents_all($node->forum_tid)) {
$parents = array_reverse($parents);
foreach ($parents as $parent) {
$breadcrumb[] = l($parent->label(), 'forum/' . $parent->id());
}
}
return $breadcrumb;
}
/**
* Builds the breadcrumb for a forum term page.
*/
protected function forumTermBreadcrumb($term) {
$vocabularies = $this->entityManager->getStorageController('taxonomy_vocabulary')->load(array($this->config->get('vocabulary')));
$vocabulary = current($vocabularies);
$breadcrumb[] = l(t('Home'), NULL);
if ($term->tid) {
// Parent of all forums is the vocabulary name.
$breadcrumb[] = l($vocabulary->label(), 'forum');
}
// Add all parent forums to breadcrumbs.
if ($term->parents) {
foreach (array_reverse($term->parents) as $parent) {
if ($parent->id() != $term->id()) {
$breadcrumb[] = l($parent->label(), 'forum/' . $parent->id());
}
}
}
return $breadcrumb;
}
}
<?php
/**
* @file
* Contains \Drupal\menu_link\MenuLinkBreadcrumbBuilder.
*/
namespace Drupal\menu_link;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
/**
* Class to define the menu_link breadcrumb builder.
*/
class MenuLinkBreadcrumbBuilder implements BreadcrumbBuilderInterface {
/**
* {@inheritdoc}
*/
public function build(array $attributes) {
// @todo Rewrite the implementation.
// Currently the result always array.
return menu_get_active_breadcrumb();
}
}
services:
menu_link.breadcrumb:
class: Drupal\menu_link\MenuLinkBreadcrumbBuilder
tags:
- { name: breadcrumb_builder, priority: 0 }
<?php
/**
* @file
* Contains \Drupal\system\LegacyBreadcrumbBuilder.
*/
namespace Drupal\system;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
/**
* Class to define the legacy breadcrumb builder.
*
* @deprecated This will be removed in 8.0. Instead, register a new breadcrumb
* builder service.
*
* @see \Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface
*
* This breadcrumb builder implements legacy support for the
* drupal_set_breadcrumb() mechanic.
* Remove this once drupal_set_breadcrumb() has been eliminated.
*/
class LegacyBreadcrumbBuilder implements BreadcrumbBuilderInterface {
/**
* {@inheritdoc}
*/
public function build(array $attributes) {
$breadcrumb = drupal_set_breadcrumb();
if (is_array($breadcrumb)) {
// $breadcrumb is expected to be an array of rendered breadcrumb links.
return $breadcrumb;
}
}
}
<?php
/**
* @file
* Contains \Drupal\system\Plugin\Block\SystemBreadcrumbBlock.
*/
namespace Drupal\system\Plugin\Block;
use Drupal\block\BlockBase;
use Drupal\Component\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
/**
* Provides a block to display the breadcrumbs.
*
* @Plugin(
* id = "system_breadcrumb_block",
* admin_label = @Translation("Breadcrumbs"),
* module = "system"
* )
*/
class SystemBreadcrumbBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
$breadcrumb_manager = \Drupal::service('breadcrumb');
$request = \Drupal::service('request');
$breadcrumb = $breadcrumb_manager->build($request->attributes->all());
if (!empty($breadcrumb)) {
// $breadcrumb is expected to be an array of rendered breadcrumb links.
return array(
'#theme' => 'breadcrumb',
'#breadcrumb' => $breadcrumb,
);
}
}
}
......@@ -10,6 +10,7 @@
use Drupal\simpletest\WebTestBase;
abstract class MenuTestBase extends WebTestBase {
/**
* Assert that a given path shows certain breadcrumb links.
*
......@@ -34,13 +35,37 @@ protected function assertBreadcrumb($goto, array $trail, $page_title = NULL, arr
if (isset($goto)) {
$this->drupalGet($goto);
}
$this->assertBreadcrumbParts($trail);
// Additionally assert page title, if given.
if (isset($page_title)) {
$this->assertTitle(strtr('@title | Drupal', array('@title' => $page_title)));
}
// Additionally assert active trail in a menu tree output, if given.
if ($tree) {
$this->assertMenuActiveTrail($tree, $last_active);
}
}
/**
* Assert that a trail exists in the internal browser.
*
* @param array $trail
* An associative array whose keys are expected breadcrumb link paths and
* whose values are expected breadcrumb link texts (not sanitized).
*/
protected function assertBreadcrumbParts($trail) {
// Compare paths with actual breadcrumb.
$parts = $this->getParts();
$parts = $this->getBreadcrumbParts();
$pass = TRUE;
foreach ($trail as $path => $title) {
$url = url($path);
$part = array_shift($parts);
$pass = ($pass && $part['href'] === $url && $part['text'] === check_plain($title));
// There may be more than one breadcrumb on the page.
while (!empty($parts)) {
foreach ($trail as $path => $title) {
$url = url($path);
$part = array_shift($parts);
$pass = ($pass && $part['href'] === $url && $part['text'] === check_plain($title));
}
}
// No parts must be left, or an expected "Home" will always pass.
$pass = ($pass && empty($parts));
......@@ -49,60 +74,65 @@ protected function assertBreadcrumb($goto, array $trail, $page_title = NULL, arr
'%parts' => implode(' » ', $trail),
'@path' => $this->getUrl(),
)));
}
// Additionally assert page title, if given.
if (isset($page_title)) {
$this->assertTitle(strtr('@title | Drupal', array('@title' => $page_title)));
}
// Additionally assert active trail in a menu tree output, if given.
/**
* Assert that active trail exists in a menu tree output.
*
* @param array $tree
* An associative array whose keys are link paths and whose
* values are link titles (not sanitized) of an expected active trail in a
* menu tree output on the page.
* @param bool $last_active
* Whether the last link in $tree is expected to be active (TRUE)
* or just to be in the active trail (FALSE).
*/
protected function assertMenuActiveTrail($tree, $last_active) {
end($tree);
$active_link_path = key($tree);
$active_link_title = array_pop($tree);
$xpath = '';
if ($tree) {
end($tree);
$active_link_path = key($tree);
$active_link_title = array_pop($tree);
$xpath = '';
if ($tree) {
$i = 0;
foreach ($tree as $link_path => $link_title) {
$part_xpath = (!$i ? '//' : '/following-sibling::ul/descendant::');
$part_xpath .= 'li[contains(@class, :class)]/a[contains(@href, :href) and contains(text(), :title)]';
$part_args = array(
':class' => 'active-trail',
':href' => url($link_path),
':title' => $link_title,
);
$xpath .= $this->buildXPathQuery($part_xpath, $part_args);
$i++;
}
$elements = $this->xpath($xpath);
$this->assertTrue(!empty($elements), 'Active trail to current page was found in menu tree.');
// Append prefix for active link asserted below.
$xpath .= '/following-sibling::ul/descendant::';
}
else {
$xpath .= '//';
$i = 0;
foreach ($tree as $link_path => $link_title) {
$part_xpath = (!$i ? '//' : '/following-sibling::ul/descendant::');
$part_xpath .= 'li[contains(@class, :class)]/a[contains(@href, :href) and contains(text(), :title)]';
$part_args = array(
':class' => 'active-trail',
':href' => url($link_path),
':title' => $link_title,
);
$xpath .= $this->buildXPathQuery($part_xpath, $part_args);
$i++;
}
$xpath_last_active = ($last_active ? 'and contains(@class, :class-active)' : '');
$xpath .= 'li[contains(@class, :class-trail)]/a[contains(@href, :href) ' . $xpath_last_active . 'and contains(text(), :title)]';
$args = array(
':class-trail' => 'active-trail',
':class-active' => 'active',
':href' => url($active_link_path),
':title' => $active_link_title,
);
$elements = $this->xpath($xpath, $args);
$this->assertTrue(!empty($elements), format_string('Active link %title was found in menu tree, including active trail links %tree.', array(
'%title' => $active_link_title,
'%tree' => implode(' » ', $tree),
)));
$elements = $this->xpath($xpath);
$this->assertTrue(!empty($elements), 'Active trail to current page was found in menu tree.');
// Append prefix for active link asserted below.
$xpath .= '/following-sibling::ul/descendant::';
}
else {
$xpath .= '//';
}
$xpath_last_active = ($last_active ? 'a