Issue #2301319 by pwolanin, effulgentsia: MenuLinkNG part5: Remove dead code and party!

8.0.x
Dries 2014-07-30 15:54:09 -04:00
parent 718a47d755
commit c77851c473
16 changed files with 0 additions and 3909 deletions

View File

@ -255,14 +255,6 @@ use Drupal\Core\Template\Attribute;
* documentation here.
*/
/**
* The maximum depth of a menu links tree - matches the number of p columns.
*
* @todo Move this constant to MenuLinkStorage along with all the tree
* functionality.
*/
const MENU_MAX_DEPTH = 9;
/**
* @section Rendering menus
* Once you have created menus (that contain menu links), you want to render
@ -315,92 +307,6 @@ const MENU_MAX_DEPTH = 9;
* @endcode
*/
/**
* Localizes a menu link title using t() if possible.
*
* Translate the title and description to allow storage of English title
* strings in the database, yet display of them in the language required
* by the current user.
*
* @param $item
* A menu link entity.
*/
function _menu_item_localize(&$item) {
// Allow default menu links to be translated.
$item['localized_options'] = $item['options'];
// All 'class' attributes are assumed to be an array during rendering, but
// links stored in the database may use an old string value.
// @todo In order to remove this code we need to implement a database update
// including unserializing all existing link options and running this code
// on them, as well as adding validation to menu_link_save().
if (isset($item['options']['attributes']['class']) && is_string($item['options']['attributes']['class'])) {
$item['localized_options']['attributes']['class'] = explode(' ', $item['options']['attributes']['class']);
}
// If the menu link is defined in code and not customized, we can use t().
if (!empty($item['machine_name']) && !$item['customized']) {
// @todo Figure out a proper way to support translations of menu links, see
// https://drupal.org/node/2193777.
$item['title'] = t($item['link_title']);
}
else {
$item['title'] = $item['link_title'];
}
}
/**
* Provides menu link unserializing, access control, and argument handling.
*
* @param array $item
* The passed in item has the following keys:
* - access: (optional) Becomes TRUE if the item is accessible, FALSE
* otherwise. If the key is not set, the access manager is used to
* determine the access.
* - options: (required) Is unserialized and copied to $item['localized_options'].
* - link_title: (required) The title of the menu link.
* - route_name: (required) The route name of the menu link.
* - route_parameters: (required) The unserialized route parameters of the menu link.
* The passed in item is changed by the following keys:
* - href: The actual path to the link. This path is generated from the
* link_path of the menu link entity.
* - title: The title of the link. This title is generated from the
* link_title of the menu link entity.
*/
function _menu_link_translate(&$item) {
if (!is_array($item['options'])) {
$item['options'] = (array) unserialize($item['options']);
}
$item['localized_options'] = $item['options'];
$item['title'] = $item['link_title'];
if ($item['external'] || empty($item['route_name'])) {
$item['access'] = 1;
$item['href'] = $item['link_path'];
$item['route_parameters'] = array();
// Set to NULL so that drupal_pre_render_link() is certain to skip it.
$item['route_name'] = NULL;
}
else {
$item['href'] = NULL;
if (!is_array($item['route_parameters'])) {
$item['route_parameters'] = (array) unserialize($item['route_parameters']);
}
// menu_tree_check_access() may set this ahead of time for links to nodes.
if (!isset($item['access'])) {
$item['access'] = \Drupal::getContainer()->get('access_manager')->checkNamedRoute($item['route_name'], $item['route_parameters'], \Drupal::currentUser());
}
// For performance, don't localize a link the user can't access.
if ($item['access']) {
_menu_item_localize($item);
}
}
// Allow other customizations - e.g. adding a page-specific query string to the
// options array. For performance reasons we only invoke this hook if the link
// has the 'alter' flag set in the options array.
if (!empty($item['options']['alter'])) {
\Drupal::moduleHandler()->alter('translated_menu_link', $item, $map);
}
}
/**
* Implements template_preprocess_HOOK() for theme_menu_tree().
*/
@ -764,119 +670,6 @@ function theme_menu_local_tasks(&$variables) {
return $output;
}
/**
* Sets (or gets) the active menu for the current page.
*
* The active menu for the page determines the active trail.
*
* @return
* An array of menu machine names, in order of preference. The
* 'system.menu:active_menus_default' config item may be used to assert a menu
* order different from the order of creation, or to prevent a particular menu
* from being used at all in the active trail.
*/
function menu_set_active_menu_names($menu_names = NULL) {
$active = &drupal_static(__FUNCTION__);
if (isset($menu_names) && is_array($menu_names)) {
$active = $menu_names;
}
elseif (!isset($active)) {
$config = \Drupal::config('system.menu');
$active = $config->get('active_menus_default') ?: array_keys(menu_list_system_menus());
}
return $active;
}
/**
* Gets the active menu for the current page.
*/
function menu_get_active_menu_names() {
return menu_set_active_menu_names();
}
/**
* Looks up the preferred menu link for a given system path.
*
* @param $path
* The path; for example, 'node/5'. The function will find the corresponding
* menu link ('node/5' if it exists, or fallback to 'node/%').
* @param $selected_menu
* The name of a menu used to restrict the search for a preferred menu link.
* If not specified, all the menus returned by menu_get_active_menu_names()
* will be used.
*
* @return
* A fully translated menu link, or FALSE if no matching menu link was
* found. The most specific menu link ('node/5' preferred over 'node/%') in
* the most preferred menu (as defined by menu_get_active_menu_names()) is
* returned.
*/
function menu_link_get_preferred($path = NULL, $selected_menu = NULL) {
$preferred_links = &drupal_static(__FUNCTION__);
if (!isset($path)) {
$path = current_path();
}
if (empty($selected_menu)) {
// Use an illegal menu name as the key for the preferred menu link.
$selected_menu = MENU_PREFERRED_LINK;
}
if (!isset($preferred_links[$path])) {
// Look for the correct menu link by building a list of candidate paths,
// which are ordered by priority (translated hrefs are preferred over
// untranslated paths). Afterwards, the most relevant path is picked from
// the menus, ordered by menu preference.
$path_candidates = array();
// 1. The current item href.
// @todo simplify this code and convert to using route names.
// @see https://drupal.org/node/2154949
$path_candidates[$path] = $path;
// Retrieve a list of menu names, ordered by preference.
$menu_names = menu_get_active_menu_names();
// Put the selected menu at the front of the list.
array_unshift($menu_names, $selected_menu);
$menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $path_candidates));
// Sort candidates by link path and menu name.
$candidates = array();
foreach ($menu_links as $candidate) {
$candidates[$candidate['link_path']][$candidate['menu_name']] = $candidate;
// Add any menus not already in the menu name search list.
if (!in_array($candidate['menu_name'], $menu_names)) {
$menu_names[] = $candidate['menu_name'];
}
}
// Store the most specific link for each menu. Also save the most specific
// link of the most preferred menu in $preferred_link.
foreach ($path_candidates as $link_path) {
if (isset($candidates[$link_path])) {
foreach ($menu_names as $menu_name) {
if (empty($preferred_links[$path][$menu_name]) && isset($candidates[$link_path][$menu_name])) {
$candidate_item = $candidates[$link_path][$menu_name];
$candidate_item['access'] = \Drupal::service('access_manager')->checkNamedRoute($candidate_item['route_name'], $candidate_item['route_parameters'], \Drupal::currentUser());
if ($candidate_item['access']) {
_menu_item_localize($candidate_item);
$preferred_links[$path][$menu_name] = $candidate_item;
if (empty($preferred_links[$path][MENU_PREFERRED_LINK])) {
// Store the most specific link.
$preferred_links[$path][MENU_PREFERRED_LINK] = $candidate_item;
}
}
}
}
}
}
}
return isset($preferred_links[$path][$selected_menu]) ? $preferred_links[$path][$selected_menu] : FALSE;
}
/**
* Clears all cached menu data.
*
@ -887,225 +680,6 @@ function menu_cache_clear_all() {
\Drupal::cache('menu')->invalidateAll();
}
/**
* Saves menu links recursively for menu_links_rebuild_defaults().
*/
function _menu_link_save_recursive($controller, $machine_name, &$children, &$links) {
$menu_link = $links[$machine_name];
if ($menu_link->isNew() || !$menu_link->customized) {
if (!isset($menu_link->plid) && !empty($menu_link->parent) && !empty($links[$menu_link->parent])) {
$parent = $links[$menu_link->parent];
if (empty($menu_link->menu_name) || $parent->menu_name == $menu_link->menu_name) {
$menu_link->plid = $parent->id();
$menu_link->menu_name = $parent->menu_name;
}
}
$controller->save($menu_link);
}
if (!empty($children[$machine_name])) {
foreach ($children[$machine_name] as $next_name) {
_menu_link_save_recursive($controller, $next_name, $children, $links);
}
}
// Remove processed link names so we can find stragglers.
unset($children[$machine_name]);
}
/**
* Builds menu links for the items returned from the menu_link.static service.
*/
function menu_link_rebuild_defaults() {
// Ensure that all configuration used to build the menu items are loaded
// without overrides.
$old_state = \Drupal::configFactory()->getOverrideState();
\Drupal::configFactory()->setOverrideState(FALSE);
$module_handler = \Drupal::moduleHandler();
if (!$module_handler->moduleExists('menu_link')) {
// The Menu link module may not be available during install, so rebuild
// when possible.
return;
}
/** @var \Drupal\menu_link\MenuLinkStorageInterface $menu_link_storage */
$menu_link_storage = \Drupal::entityManager()
->getStorage('menu_link');
$links = array();
$children = array();
$top_links = array();
$all_links = \Drupal::service('menu_link.static')->getLinks();
if ($all_links) {
foreach ($all_links as $machine_name => $link) {
// For performance reasons, do a straight query now and convert to a menu
// link entity later.
// @todo revisit before release.
$existing_item = db_select('menu_links')
->fields('menu_links')
->condition('machine_name', $machine_name)
->execute()->fetchObject();
if ($existing_item) {
$existing_item->options = unserialize($existing_item->options);
$existing_item->route_parameters = unserialize($existing_item->route_parameters);
$link['mlid'] = $existing_item->mlid;
$link['plid'] = $existing_item->plid;
$link['uuid'] = $existing_item->uuid;
$link['customized'] = $existing_item->customized;
$link['updated'] = $existing_item->updated;
$menu_link = $menu_link_storage->createFromDefaultLink($link);
// @todo Do not create a new entity in order to update it, see
// https://drupal.org/node/2241865
$menu_link->setOriginalId($existing_item->mlid);
// Convert the existing item to a typed object.
/** @var \Drupal\menu_link\MenuLinkInterface $existing_item */
$existing_item = $menu_link_storage->create(get_object_vars($existing_item));
if (!$existing_item->customized) {
// A change in the default menu links may move the link to a
// different menu or parent.
if (!empty($link['menu_name']) && ($link['menu_name'] != $existing_item->menu_name)) {
$menu_link->plid = NULL;
$menu_link->menu_name = $link['menu_name'];
}
elseif (!empty($link['parent'])) {
$menu_link->plid = NULL;
}
$menu_link->original = $existing_item;
}
}
else {
if (empty($link['route_name']) && empty($link['link_path'])) {
\Drupal::logger('menu_link')->error('Menu_link %machine_name does neither provide a route_name nor a link_path, so it got skipped.', array('%machine_name' => $machine_name));
continue;
}
$menu_link = $menu_link_storage->createFromDefaultLink($link);
}
if (!empty($link['parent'])) {
$children[$link['parent']][$machine_name] = $machine_name;
$menu_link->parent = $link['parent'];
if (empty($link['menu_name'])) {
// Reset the default menu name so it is populated from the parent.
$menu_link->menu_name = NULL;
}
}
else {
// A top level link - we need them to root our tree.
$top_links[$machine_name] = $machine_name;
$menu_link->plid = 0;
}
$links[$machine_name] = $menu_link;
}
}
foreach ($top_links as $machine_name) {
_menu_link_save_recursive($menu_link_storage, $machine_name, $children, $links);
}
// Handle any children we didn't find starting from top-level links.
foreach ($children as $orphan_links) {
foreach ($orphan_links as $machine_name) {
// Force it to the top level.
$links[$machine_name]->plid = 0;
_menu_link_save_recursive($menu_link_storage, $machine_name, $children, $links);
}
}
// Find any item whose default menu link no longer exists.
if ($all_links) {
$query = \Drupal::entityQuery('menu_link')
->condition('machine_name', array_keys($all_links), 'NOT IN')
->exists('machine_name')
->condition('external', 0)
->condition('updated', 0)
->condition('customized', 0)
->sort('depth', 'DESC');
$result = $query->execute();
}
else {
$result = array();
}
// Remove all such items. Starting from those with the greatest depth will
// minimize the amount of re-parenting done by the menu link controller.
if ($result) {
menu_link_delete_multiple($result, TRUE);
}
\Drupal::configFactory()->setOverrideState($old_state);
}
/**
* Returns an array containing all links for a menu.
*
* @param $menu_name
* The name of the menu whose links should be returned.
*
* @return
* An array of menu links.
*/
function menu_load_links($menu_name) {
$links = array();
$query = \Drupal::entityQuery('menu_link')
->condition('menu_name', $menu_name)
// Order by weight so as to be helpful for menus that are only one level
// deep.
->sort('weight');
$result = $query->execute();
if (!empty($result)) {
$links = menu_link_load_multiple($result);
}
return $links;
}
/**
* Deletes all links for a menu.
*
* @param $menu_name
* The name of the menu whose links will be deleted.
*/
function menu_delete_links($menu_name) {
$links = menu_load_links($menu_name);
menu_link_delete_multiple(array_keys($links), FALSE, TRUE);
}
/**
* Updates the expanded menu item state at most twice per page load.
*/
function _menu_update_expanded_menus() {
$expanded_menus_updated = &drupal_static(__FUNCTION__, 0);
// Update the expanded menu item state, but at most twice, including at
// the end of the page load when there are multiple links saved or deleted.
if ($expanded_menus_updated == 0) {
// Keep track of which menus have expanded items.
_menu_set_expanded_menus();
$expanded_menus_updated = 1;
}
elseif ($expanded_menus_updated == 1) {
// Keep track of which menus have expanded items.
drupal_register_shutdown_function('_menu_set_expanded_menus');
$expanded_menus_updated = 2;
}
}
/**
* Updates a list of menus with expanded items.
*/
function _menu_set_expanded_menus() {
$names = array();
$result = Drupal::entityQueryAggregate('menu_link')
->condition('expanded', 0, '<>')
->groupBy('menu_name')
->execute();
// Flatten the resulting array.
foreach($result as $k => $v) {
$names[$k] = $v['menu_name'];
}
\Drupal::state()->set('menu_expanded', $names);
}
/**
* @} End of "defgroup menu".
*/

View File

@ -1,42 +0,0 @@
<?php
/**
* @file
* Hooks provided by the Menu link module.
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Alter a menu link after it has been translated and before it is rendered.
*
* This hook is invoked from _menu_link_translate() after a menu link has been
* translated; i.e., after the user access to the link's target page has
* been checked. It is only invoked if $menu_link['options']['alter'] has been
* set to a non-empty value (e.g. TRUE). This flag should be set using
* hook_ENTITY_TYPE_presave() for entity 'menu_link'.
*
* Implementations of this hook are able to alter any property of the menu link.
* For example, this hook may be used to add a page-specific query string to all
* menu links, or hide a certain link by setting:
* @code
* 'hidden' => 1,
* @endcode
*
* @param \Drupal\menu_link\Entity\MenuLink $menu_link
* A menu link entity.
*
* @see hook_menu_link_alter()
*/
function hook_translated_menu_link_alter(\Drupal\menu_link\Entity\MenuLink &$menu_link, $map) {
if ($menu_link->href == 'devel/cache/clear') {
$menu_link->localized_options['query'] = drupal_get_destination();
}
}
/**
* @} End of "addtogroup hooks".
*/

View File

@ -1,221 +0,0 @@
<?php
/**
* @file
* Install, update and uninstall functions for the menu_link module.
*/
/**
* Implements hook_schema().
*/
function menu_link_schema() {
$schema['menu_links'] = array(
'description' => 'Contains the individual links within a menu.',
'fields' => array(
'menu_name' => array(
'description' => "The menu name. All links with the same menu name (such as 'tools') are part of the same menu.",
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
),
'mlid' => array(
'description' => 'The menu link ID (mlid) is the integer primary key.',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
),
'uuid' => array(
'description' => 'Unique Key: Universally unique identifier for this entity.',
'type' => 'varchar',
'length' => 128,
'not null' => FALSE,
),
'machine_name' => array(
'description' => 'Unique machine name: Optional human-readable ID for this link.',
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
),
'plid' => array(
'description' => 'The parent link ID (plid) is the mlid of the link above in the hierarchy, or zero if the link is at the top level in its menu.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'link_path' => array(
'description' => 'The Drupal path or external path this link points to.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
'langcode' => array(
'description' => 'The {language}.langcode of this link.',
'type' => 'varchar',
'length' => 12,
'not null' => TRUE,
'default' => '',
),
'link_title' => array(
'description' => 'The text displayed for the link.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
'options' => array(
'description' => 'A serialized array of options to be passed to the url() or l() function, such as a query string or HTML attributes.',
'type' => 'blob',
'not null' => FALSE,
'serialize' => TRUE,
),
'module' => array(
'description' => 'The name of the module that generated this link.',
'type' => 'varchar',
'length' => DRUPAL_EXTENSION_NAME_MAX_LENGTH,
'not null' => TRUE,
'default' => 'system',
),
'hidden' => array(
'description' => 'A flag for whether the link should be rendered in menus. (1 = a disabled menu item that may be shown on admin screens, -1 = a menu callback, 0 = a normal, visible link)',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
'external' => array(
'description' => 'A flag to indicate if the link points to a full URL starting with a protocol, like http:// (1 = external, 0 = internal).',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
'has_children' => array(
'description' => 'Flag indicating whether any links have this link as a parent (1 = children exist, 0 = no children).',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
'expanded' => array(
'description' => 'Flag for whether this link should be rendered as expanded in menus - expanded links always have their child links displayed, instead of only when the link is in the active trail (1 = expanded, 0 = not expanded)',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
'weight' => array(
'description' => 'Link weight among links in the same menu at the same depth.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'depth' => array(
'description' => 'The depth relative to the top level. A link with plid == 0 will have depth == 1.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
'customized' => array(
'description' => 'A flag to indicate that the user has manually created or edited the link (1 = customized, 0 = not customized).',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
'p1' => array(
'description' => 'The first mlid in the materialized path. If N = depth, then pN must equal the mlid. If depth > 1 then p(N-1) must equal the plid. All pX where X > depth must equal zero. The columns p1 .. p9 are also called the parents.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p2' => array(
'description' => 'The second mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p3' => array(
'description' => 'The third mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p4' => array(
'description' => 'The fourth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p5' => array(
'description' => 'The fifth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p6' => array(
'description' => 'The sixth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p7' => array(
'description' => 'The seventh mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p8' => array(
'description' => 'The eighth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'p9' => array(
'description' => 'The ninth mlid in the materialized path. See p1.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
),
'updated' => array(
'description' => 'Flag that indicates that this link was generated during the update from Drupal 5.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
),
'route_name' => array(
'description' => 'The machine name of a defined Symfony Route this menu item represents.',
'type' => 'varchar',
'length' => 255,
),
'route_parameters' => array(
'description' => 'Serialized array of route parameters of this menu link.',
'type' => 'blob',
'size' => 'big',
'not null' => FALSE,
'serialize' => TRUE,
),
),
'indexes' => array(
'path_menu' => array(array('link_path', 128), 'menu_name'),
'menu_plid_expand_child' => array('menu_name', 'plid', 'expanded', 'has_children'),
'menu_parents' => array('menu_name', 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9'),
),
'primary key' => array('mlid'),
);
return $schema;
}

View File

@ -1,225 +0,0 @@
<?php
/**
* @file
* Enables users to create menu links.
*/
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\menu_link\Entity\MenuLink;
use Drupal\menu_link\MenuLinkInterface;
function menu_link_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.menu_link':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Menu Link module allows users to create menu links. It is required by the Menu UI module, which provides an interface for managing menus. See the <a href="!menu-help">Menu UI module help page</a> for more information.', array('!menu-help' => \Drupal::url('help.page', array('name' => 'menu_ui')))) . '</p>';
return $output;
}
}
/**
* Entity URI callback.
*
* @param \Drupal\menu_link\Entity\MenuLink $menu_link
* A menu link entity.
*/
function menu_link_uri(MenuLink $menu_link) {
return new Url($menu_link->route_name, $menu_link->route_parameters);
}
/**
* Loads a menu link entity.
*
* This function should never be called from within node_load() or any other
* function used as a menu object load function since an infinite recursion may
* occur.
*
* @param int $mlid
* The menu link ID.
* @param bool $reset
* (optional) Whether to reset the menu_link_load_multiple() cache.
*
* @return \Drupal\menu_link\Entity\MenuLink|null
* A menu link entity, or NULL if there is no entity with the given ID.
*
* @deprecated in Drupal 8.x, will be removed before Drupal 9.0.
* Use \Drupal\menu_link\Entity\MenuLink::load().
*/
function menu_link_load($mlid = NULL, $reset = FALSE) {
if ($reset) {
\Drupal::entityManager()->getStorage('menu_link')->resetCache(array($mlid));
}
return MenuLink::load($mlid);
}
/**
* Loads menu link entities from the database.
*
* @param array $mlids
* (optional) An array of entity IDs. If omitted, all entities are loaded.
* @param bool $reset
* (optional) Whether to reset the internal cache.
*
* @return array<\Drupal\menu_link\Entity\MenuLink>
* An array of menu link entities indexed by entity IDs.
*
* @see menu_link_load()
* @see entity_load_multiple()
*
* @deprecated in Drupal 8.x, will be removed before Drupal 9.0.
* Use \Drupal\menu_link\Entity\MenuLink::loadMultiple().
*/
function menu_link_load_multiple(array $mlids = NULL, $reset = FALSE) {
if ($reset) {
\Drupal::entityManager()->getStorage('menu_link')->resetCache($mlids);
}
return MenuLink::loadMultiple($mlids);
}
/**
* Deletes a menu link.
*
* @param int $mlid
* The menu link ID.
*
* @see menu_link_delete_multiple()
*/
function menu_link_delete($mlid) {
menu_link_delete_multiple(array($mlid));
}
/**
* Deletes multiple menu links.
*
* @param array $mlids
* An array of menu link IDs.
* @param bool $force
* (optional) Forces deletion. Internal use only, setting to TRUE is
* discouraged. Defaults to FALSE.
* @param bool $prevent_reparenting
* (optional) Disables the re-parenting logic from the deletion process.
* Defaults to FALSE.
*/
function menu_link_delete_multiple(array $mlids, $force = FALSE, $prevent_reparenting = FALSE) {
if (!$mlids) {
// If no IDs or invalid IDs were passed, do nothing.
return;
}
$controller = \Drupal::entityManager()
->getStorage('menu_link');
if (!$force) {
$entity_query = \Drupal::entityQuery('menu_link');
$group = $entity_query->orConditionGroup()
->condition('module', 'system', '<>')
->condition('updated', 0, '<>');
$entity_query->condition('mlid', $mlids, 'IN');
$entity_query->condition($group);
$result = $entity_query->execute();
$entities = $controller->loadMultiple($result);
}
else {
$entities = $controller->loadMultiple($mlids);
}
$controller->setPreventReparenting($prevent_reparenting);
$controller->delete($entities);
}
/**
* Saves a menu link.
*
* After calling this function, rebuild the menu cache using
* menu_cache_clear_all().
*
* @param \Drupal\menu_link\Entity\MenuLink $menu_link
* The menu link entity to be saved.
*
* @return int|bool
* Returns SAVED_NEW or SAVED_UPDATED if the save operation succeeded, or
* FALSE if it failed.
*/
function menu_link_save(MenuLink $menu_link) {
return $menu_link->save();
}
/**
* Inserts, updates, enables, disables, or deletes an uncustomized menu link.
*
* @param string $module
* The name of the module that owns the link.
* @param string $op
* Operation to perform: insert, update, enable, disable, or delete.
* @param string $link_path
* The path this link points to.
* @param string $link_title
* (optional) Title of the link to insert or new title to update the link to.
* Unused for delete. Defaults to NULL.
*
* @return integer|null
* The insert op returns the mlid of the new item. Others op return NULL.
*/
function menu_link_maintain($module, $op, $link_path, $link_title = NULL) {
$menu_link_controller = \Drupal::entityManager()
->getStorage('menu_link');
switch ($op) {
case 'insert':
$menu_link = entity_create('menu_link', array(
'link_title' => $link_title,
'link_path' => $link_path,
'module' => $module,)
);
return $menu_link->save();
case 'update':
$menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $link_path, 'module' => $module, 'customized' => 0));
foreach ($menu_links as $menu_link) {
$menu_link->original = clone $menu_link;
if (isset($link_title)) {
$menu_link->link_title = $link_title;
}
$menu_link_controller->save($menu_link);
}
break;
case 'enable':
case 'disable':
$menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $link_path, 'module' => $module, 'customized' => 0));
foreach ($menu_links as $menu_link) {
$menu_link->original = clone $menu_link;
$menu_link->hidden = ($op == 'disable' ? 1 : 0);
$menu_link->customized = 1;
if (isset($link_title)) {
$menu_link->link_title = $link_title;
}
$menu_link_controller->save($menu_link);
}
break;
case 'delete':
$result = \Drupal::entityQuery('menu_link')->condition('link_path', $link_path)->execute();
if (!empty($result)) {
menu_link_delete_multiple($result);
}
break;
}
}
/**
* Implements hook_system_breadcrumb_alter().
*/
function menu_link_system_breadcrumb_alter(array &$breadcrumb, RouteMatchInterface $route_match, array $context) {
// Custom breadcrumb behavior for editing menu links, we append a link to
// the menu in which the link is found.
if (($route_match->getRouteName() == 'menu_ui.link_edit') && $menu_link = $route_match->getParameter('menu_link')) {
if (($menu_link instanceof MenuLinkInterface) && !$menu_link->isNew()) {
// Add a link to the menu admin screen.
$menu = entity_load('menu', $menu_link->menu_name);
$breadcrumb[] = Drupal::l($menu->label(), 'menu_ui.menu_edit', array('menu' => $menu->id));
}
}
}

View File

@ -1,7 +0,0 @@
services:
menu_link.tree:
class: Drupal\menu_link\MenuTree
arguments: ['@database', '@cache.data', '@language_manager', '@request_stack', '@entity.manager', '@entity.query', '@state']
menu_link.static:
class: Drupal\menu_link\StaticMenuLinks
arguments: ['@module_handler']

View File

@ -1,680 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\menu_link\Entity\MenuLink.
*/
namespace Drupal\menu_link\Entity;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\Entity;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Url;
use Drupal\menu_link\MenuLinkInterface;
use Symfony\Component\Routing\Route;
/**
* Defines the menu link entity class.
*
* @EntityType(
* id = "menu_link",
* label = @Translation("Menu link"),
* controllers = {
* "storage" = "Drupal\menu_link\MenuLinkStorage",
* "access" = "Drupal\menu_link\MenuLinkAccessController",
* "form" = {
* "default" = "Drupal\menu_link\MenuLinkForm"
* }
* },
* admin_permission = "administer menu",
* static_cache = FALSE,
* base_table = "menu_links",
* uri_callback = "menu_link_uri",
* translatable = TRUE,
* entity_keys = {
* "id" = "mlid",
* "label" = "link_title",
* "uuid" = "uuid",
* "bundle" = "bundle"
* },
* )
*/
class MenuLink extends Entity implements \ArrayAccess, MenuLinkInterface {
/**
* The link's menu name.
*
* @var string
*/
public $menu_name = 'tools';
/**
* The link's bundle.
*
* @var string
*/
public $bundle = 'tools';
/**
* The menu link ID.
*
* @var int
*/
public $mlid;
/**
* An optional machine name if defined via the menu_link.static service.
*
* @var string
*/
public $machine_name;
/**
* The menu link UUID.
*
* @var string
*/
public $uuid;
/**
* The parent link ID.
*
* @var int
*/
public $plid;
/**
* The Drupal path or external path this link points to.
*
* @var string
*/
public $link_path;
/**
* The entity label.
*
* @var string
*/
public $link_title = '';
/**
* A serialized array of options to be passed to the url() or l() function,
* such as a query string or HTML attributes.
*
* @var array
*/
public $options = array();
/**
* The name of the module that generated this link.
*
* @var string
*/
public $module = 'menu_ui';
/**
* A flag for whether the link should be rendered in menus.
*
* @var int
*/
public $hidden = 0;
/**
* A flag to indicate if the link points to a full URL starting with a
* protocol, like http:// (1 = external, 0 = internal).
*
* @var int
*/
public $external;
/**
* Flag indicating whether any links have this link as a parent.
*
* @var int
*/
public $has_children = 0;
/**
* Flag for whether this link should be rendered as expanded in menus.
* Expanded links always have their child links displayed, instead of only
* when the link is in the active trail.
*
* @var int
*/
public $expanded = 0;
/**
* Link weight among links in the same menu at the same depth.
*
* @var int
*/
public $weight = 0;
/**
* The depth relative to the top level. A link with plid == 0 will have
* depth == 1.
*
* @var int
*/
public $depth;
/**
* A flag to indicate that the user has manually created or edited the link.
*
* @var int
*/
public $customized = 0;
/**
* The first entity ID in the materialized path.
*
* @var int
*
* @todo Investigate whether the p1, p2, .. pX properties can be moved to a
* single array property.
*/
public $p1;
/**
* The second entity ID in the materialized path.
*
* @var int
*/
public $p2;
/**
* The third entity ID in the materialized path.
*
* @var int
*/
public $p3;
/**
* The fourth entity ID in the materialized path.
*
* @var int
*/
public $p4;
/**
* The fifth entity ID in the materialized path.
*
* @var int
*/
public $p5;
/**
* The sixth entity ID in the materialized path.
*
* @var int
*/
public $p6;
/**
* The seventh entity ID in the materialized path.
*
* @var int
*/
public $p7;
/**
* The eighth entity ID in the materialized path.
*
* @var int
*/
public $p8;
/**
* The ninth entity ID in the materialized path.
*
* @var int
*/
public $p9;
/**
* The menu link modification timestamp.
*
* @var int
*/
public $updated = 0;
/**
* The name of the route associated with this menu link, if any.
*
* @var string
*/
public $route_name;
/**
* The parameters of the route associated with this menu link, if any.
*
* @var array
*/
public $route_parameters = array();
/**
* The route object associated with this menu link, if any.
*
* @var \Symfony\Component\Routing\Route
*/
protected $routeObject;
/**
* Boolean indicating whether a new revision should be created on save.
*
* @var bool
*/
protected $newRevision = FALSE;
/**
* Indicates whether this is the default revision.
*
* @var bool
*/
protected $isDefaultRevision = TRUE;
/**
* {@inheritdoc}
*/
public function setNewRevision($value = TRUE) {
$this->newRevision = $value;
}
/**
* {@inheritdoc}
*/
public function isNewRevision() {
return $this->newRevision || ($this->getEntityType()->hasKey('revision') && !$this->getRevisionId());
}
/**
* {@inheritdoc}
*/
public function getRevisionId() {
return NULL;
}
/**
* {@inheritdoc}
*/
public function isTranslatable() {
// @todo Inject the entity manager and retrieve bundle info from it.
$bundles = entity_get_bundles($this->entityTypeId);
return !empty($bundles[$this->bundle()]['translatable']);
}
/**
* {@inheritdoc}
*/
public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) {
}
/**
* Overrides Entity::id().
*/
public function id() {
return $this->mlid;
}
/**
* {@inheritdoc}
*/
public function bundle() {
return $this->bundle;
}
/**
* Overrides Entity::createDuplicate().
*/
public function createDuplicate() {
$duplicate = parent::createDuplicate();
$duplicate->plid = NULL;
return $duplicate;
}
/**
* {@inheritdoc}
*/
public function getRoute() {
if (!$this->route_name) {
return NULL;
}
if (!($this->routeObject instanceof Route)) {
$route_provider = \Drupal::service('router.route_provider');
$this->routeObject = $route_provider->getRouteByName($this->route_name);
}
return $this->routeObject;
}
/**
* {@inheritdoc}
*/
public function setRouteObject(Route $route) {
$this->routeObject = $route;
}
/**
* {@inheritdoc}
*/
public function reset() {
// To reset the link to its original values, we need to retrieve its
// definition from the menu_link.static service. Otherwise, for example,
// the link's menu would not be reset, because properties like the original
// 'menu_name' are not stored anywhere else. Since resetting a link happens
// rarely and this is a one-time operation, retrieving the full set of
// default menu links does little harm.
$all_links = \Drupal::service('menu_link.static')->getLinks();
$original = $all_links[$this->machine_name];
$original['machine_name'] = $this->machine_name;
/** @var \Drupal\menu_link\MenuLinkStorageInterface $storage */
$storage = \Drupal::entityManager()->getStorage($this->entityTypeId);
// @todo Do not create a new entity in order to update it, see
// https://drupal.org/node/2241865
$new_link = $storage->createFromDefaultLink($original);
$new_link->setOriginalId($this->id());
// Allow the menu to be determined by the parent
if (!empty($new_link['parent']) && !empty($all_links[$new_link['parent']])) {
// Walk up the tree to find the menu name.
$parent = $all_links[$new_link['parent']];
$existing_parent = db_select('menu_links')
->fields('menu_links')
->condition('machine_name', $parent['machine_name'])
->execute()->fetchAssoc();
if ($existing_parent) {
/** @var \Drupal\Core\Entity\EntityInterface $existing_parent */
$existing_parent = $storage->create($existing_parent);
$new_link->menu_name = $existing_parent->menu_name;
$new_link->plid = $existing_parent->id();
}
}
// Merge existing menu link's ID and 'has_children' property.
foreach (array('mlid', 'has_children') as $key) {
$new_link->{$key} = $this->{$key};
}
$new_link->save();
return $new_link;
}
/**
* Implements ArrayAccess::offsetExists().
*/
public function offsetExists($offset) {
return isset($this->{$offset});
}
/**
* Implements ArrayAccess::offsetGet().
*/
public function &offsetGet($offset) {
return $this->{$offset};
}
/**
* Implements ArrayAccess::offsetSet().
*/
public function offsetSet($offset, $value) {
$this->{$offset} = $value;
}
/**
* Implements ArrayAccess::offsetUnset().
*/
public function offsetUnset($offset) {
unset($this->{$offset});
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageInterface $storage, array $entities) {
parent::preDelete($storage, $entities);
// Nothing to do if we don't want to reparent children.
if ($storage->getPreventReparenting()) {
return;
}
foreach ($entities as $entity) {
// Children get re-attached to the item's parent.
if ($entity->has_children) {
$children = $storage->loadByProperties(array('plid' => $entity->plid));
foreach ($children as $child) {
$child->plid = $entity->plid;
$storage->save($child);
}
}
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
// Update the has_children status of the parent.
foreach ($entities as $entity) {
if (!$storage->getPreventReparenting()) {
$storage->updateParentalStatus($entity);
}
}
// Also clear the menu system static caches.
menu_reset_static_cache();
_menu_update_expanded_menus();
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
// This is the easiest way to handle the unique internal path '<front>',
// since a path marked as external does not need to match a route.
$this->external = (UrlHelper::isExternal($this->link_path) || $this->link_path == '<front>') ? 1 : 0;
// Try to find a parent link. If found, assign it and derive its menu.
$parent = $this->findParent($storage);
if ($parent) {
$this->plid = $parent->id();
$this->menu_name = $parent->menu_name;
}
// If no corresponding parent link was found, move the link to the top-level.
else {
$this->plid = 0;
}
// Directly fill parents for top-level links.
if ($this->plid == 0) {
$this->p1 = $this->id();
for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) {
$parent_property = "p$i";
$this->{$parent_property} = 0;
}
$this->depth = 1;
}
// Otherwise, ensure that this link's depth is not beyond the maximum depth
// and fill parents based on the parent link.
else {
if ($this->has_children && $this->original) {
$limit = MENU_MAX_DEPTH - $storage->findChildrenRelativeDepth($this->original) - 1;
}
else {
$limit = MENU_MAX_DEPTH - 1;
}
if ($parent->depth > $limit) {
return FALSE;
}
$this->depth = $parent->depth + 1;
$this->setParents($parent);
}
// Need to check both plid and menu_name, since plid can be 0 in any menu.
if (isset($this->original) && ($this->plid != $this->original->plid || $this->menu_name != $this->original->menu_name)) {
$storage->moveChildren($this);
}
// Find the route_name.
if (!$this->external && !isset($this->route_name)) {
$url = Url::createFromPath($this->link_path);
$this->route_name = $url->getRouteName();
$this->route_parameters = $url->getRouteParameters();
}
elseif (empty($this->link_path)) {
$this->link_path = \Drupal::urlGenerator()->getPathFromRoute($this->route_name, $this->route_parameters);
}
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
// Check the has_children status of the parent.
$storage->updateParentalStatus($this);
// Entity::postSave() calls Entity::invalidateTagsOnSave(), which only
// handles the regular cases. The MenuLink entity has two special cases.
$cache_tags = array();
// Case 1: a newly created menu link is *also* added to a menu, so we must
// invalidate the associated menu's cache tag.
if (!$update) {
$cache_tags = $this->getCacheTag();
}
// Case 2: a menu link may be moved from one menu to another; the original
// menu's cache tag must also be invalidated.
if (isset($this->original) && $this->menu_name != $this->original->menu_name) {
$cache_tags = NestedArray::mergeDeep($cache_tags, $this->original->getCacheTag());
}
Cache::invalidateTags($cache_tags);
// Also clear the menu system static caches.
menu_reset_static_cache();
// Track which menu items are expanded.
_menu_update_expanded_menus();
}
/**
* {@inheritdoc}
*/
public static function postLoad(EntityStorageInterface $storage, array &$entities) {
parent::postLoad($storage, $entities);
$routes = array();
foreach ($entities as $menu_link) {
$menu_link->options = unserialize($menu_link->options);
$menu_link->route_parameters = unserialize($menu_link->route_parameters);
// By default use the menu_name as type.
$menu_link->bundle = $menu_link->menu_name;
// For all links that have an associated route, load the route object now
// and save it on the object. That way we avoid a select N+1 problem later.
if ($menu_link->route_name) {
$routes[$menu_link->id()] = $menu_link->route_name;
}
}
// Now mass-load any routes needed and associate them.
if ($routes) {
$route_objects = \Drupal::service('router.route_provider')->getRoutesByNames($routes);
foreach ($routes as $entity_id => $route) {
// Not all stored routes will be valid on load.
if (isset($route_objects[$route])) {
$entities[$entity_id]->setRouteObject($route_objects[$route]);
}
}
}
}
/**
* {@inheritdoc}
*/
protected function setParents(MenuLinkInterface $parent) {
$i = 1;
while ($i < $this->depth) {
$p = 'p' . $i++;
$this->{$p} = $parent->{$p};
}
$p = 'p' . $i++;
// The parent (p1 - p9) corresponding to the depth always equals the mlid.
$this->{$p} = $this->id();
while ($i <= MENU_MAX_DEPTH) {
$p = 'p' . $i++;
$this->{$p} = 0;
}
}
/**
* {@inheritdoc}
*/
protected function findParent(EntityStorageInterface $storage) {
$parent = FALSE;
// This item is explicitly top-level, skip the rest of the parenting.
if (isset($this->plid) && empty($this->plid)) {
return $parent;
}
// If we have a parent link ID, try to use that.
$candidates = array();
if (isset($this->plid)) {
$candidates[] = $this->plid;
}
// Else, if we have a link hierarchy try to find a valid parent in there.
if (!empty($this->depth) && $this->depth > 1) {
for ($depth = $this->depth - 1; $depth >= 1; $depth--) {
$parent_property = "p$depth";
$candidates[] = $this->$parent_property;
}
}
foreach ($candidates as $mlid) {
$parent = $storage->load($mlid);
if ($parent) {
break;
}
}
return $parent;
}
/**
* Builds and returns the renderable array for this menu link.
*
* @return array
* A renderable array representing the content of the link.
*/
public function build() {
$build = array(
'#type' => 'link',
'#title' => $this->title,
'#href' => $this->href,
'#route_name' => $this->route_name ? $this->route_name : NULL,
'#route_parameters' => $this->route_parameters,
'#options' => !empty($this->localized_options) ? $this->localized_options : array(),
);
return $build;
}
/**
* {@inheritdoc}
*/
public function getCacheTag() {
return entity_load('menu', $this->menu_name)->getCacheTag();
}
/**
* {@inheritdoc}
*/
public function getListCacheTags() {
return entity_load('menu', $this->menu_name)->getListCacheTags();
}
}

View File

@ -1,41 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\menu_link\MenuLinkAccessController.
*/
namespace Drupal\menu_link;
use Drupal\Core\Entity\EntityAccessController;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines an access controller for the menu link entity.
*
* @see \Drupal\menu_link\Entity\MenuLink
*/
class MenuLinkAccessController extends EntityAccessController {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
$access = $account->hasPermission('administer menu');
if ($access) {
switch ($operation) {
case 'reset':
// Reset allowed for items defined via hook_menu() and customized.
return !empty($entity->machine_name) && $entity->customized;
case 'delete':
// Only items created by the Menu UI module can be deleted.
return $entity->module == 'menu_ui' || $entity->updated == 1;
}
}
return $access;
}
}

View File

@ -1,311 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\menu_link\MenuLinkForm.
*/
namespace Drupal\menu_link;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Path\AliasManagerInterface;
use Drupal\Core\Routing\UrlGenerator;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form controller for the node edit forms.
*/
class MenuLinkForm extends EntityForm {
/**
* The menu link storage.
*
* @var \Drupal\menu_link\MenuLinkStorageInterface
*/
protected $menuLinkStorage;
/**
* The path alias manager.
*
* @var \Drupal\Core\Path\AliasManagerInterface
*/
protected $pathAliasManager;
/**
* The URL generator.
*
* @var \Drupal\Core\Routing\UrlGenerator
*/
protected $urlGenerator;
/**
* Constructs a new MenuLinkForm object.
*
* @param \Drupal\menu_link\MenuLinkStorageInterface $menu_link_storage
* The menu link storage.
* @param \Drupal\Core\Path\AliasManagerInterface $path_alias_manager
* The path alias manager.
* @param \Drupal\Core\Routing\UrlGenerator $url_generator
* The URL generator.
*/
public function __construct(MenuLinkStorageInterface $menu_link_storage, AliasManagerInterface $path_alias_manager, UrlGenerator $url_generator) {
$this->menuLinkStorage = $menu_link_storage;
$this->pathAliasManager = $path_alias_manager;
$this->urlGenerator = $url_generator;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.manager')->getStorage('menu_link'),
$container->get('path.alias_manager'),
$container->get('url_generator')
);
}
/**
* Overrides EntityForm::form().
*/
public function form(array $form, array &$form_state) {
$menu_link = $this->entity;
// Since menu_link_load() no longer returns a translated and access checked
// item, do it here instead.
_menu_link_translate($menu_link);
$form['link_title'] = array(
'#type' => 'textfield',
'#title' => t('Menu link title'),
'#default_value' => $menu_link->link_title,
'#description' => t('The text to be used for this link in the menu.'),
'#required' => TRUE,
);
foreach (array('link_path', 'mlid', 'module', 'has_children', 'options') as $key) {
$form[$key] = array('#type' => 'value', '#value' => $menu_link->{$key});
}
// Any item created or edited via this interface is considered "customized".
$form['customized'] = array('#type' => 'value', '#value' => 1);
// We are not using url() when constructing this path because it would add
// $base_path.
$path = $menu_link->link_path;
if (isset($menu_link->options['query'])) {
$path .= '?' . $this->urlGenerator->httpBuildQuery($menu_link->options['query']);
}
if (isset($menu_link->options['fragment'])) {
$path .= '#' . $menu_link->options['fragment'];
}
if ($menu_link->module == 'menu_ui') {
$form['link_path'] = array(
'#type' => 'textfield',
'#title' => t('Path'),
'#maxlength' => 255,
'#default_value' => $path,
'#description' => t('The path for this menu link. This can be an internal Drupal path such as %add-node or an external URL such as %drupal. Enter %front to link to the front page.', array('%front' => '<front>', '%add-node' => 'node/add', '%drupal' => 'http://drupal.org')),
'#required' => TRUE,
);
}
else {
$form['_path'] = array(
'#type' => 'item',
'#title' => t('Path'),
'#description' => l($menu_link->link_title, $menu_link->href, $menu_link->options),
);
}
$form['description'] = array(
'#type' => 'textarea',
'#title' => t('Description'),
'#default_value' => isset($menu_link->options['attributes']['title']) ? $menu_link->options['attributes']['title'] : '',
'#rows' => 1,
'#description' => t('Shown when hovering over the menu link.'),
);
$form['enabled'] = array(
'#type' => 'checkbox',
'#title' => t('Enabled'),
'#default_value' => !$menu_link->hidden,
'#description' => t('Menu links that are not enabled will not be listed in any menu.'),
);
$form['expanded'] = array(
'#type' => 'checkbox',
'#title' => t('Show as expanded'),
'#default_value' => $menu_link->expanded,
'#description' => t('If selected and this menu link has children, the menu will always appear expanded.'),
);
// Generate a list of possible parents (not including this link or descendants).
$options = menu_ui_parent_options(menu_ui_get_menus(), $menu_link);
$default = $menu_link->menu_name . ':' . $menu_link->plid;
if (!isset($options[$default])) {
$default = 'tools:0';
}
$form['parent'] = array(
'#type' => 'select',
'#title' => t('Parent link'),
'#default_value' => $default,
'#options' => $options,
'#description' => t('The maximum depth for a link and all its children is fixed at !maxdepth. Some menu links may not be available as parents if selecting them would exceed this limit.', array('!maxdepth' => MENU_MAX_DEPTH)),
'#attributes' => array('class' => array('menu-title-select')),
);
// Get number of items in menu so the weight selector is sized appropriately.
$delta = $this->menuLinkStorage->countMenuLinks($menu_link->menu_name);
$form['weight'] = array(
'#type' => 'weight',
'#title' => t('Weight'),
// Old hardcoded value.
'#delta' => max($delta, 50),
'#default_value' => $menu_link->weight,
'#description' => t('Optional. In the menu, the heavier links will sink and the lighter links will be positioned nearer the top.'),
);
// Language module allows to configure the menu link language independently
// of the menu language. It also allows to optionally show the language
// selector on the menu link form so that the language of each menu link can
// be configured individually.
if ($this->moduleHandler->moduleExists('language')) {
$language_configuration = language_get_default_configuration('menu_link', $menu_link->bundle());
$default_langcode = ($menu_link->isNew() ? $language_configuration['langcode'] : $menu_link->langcode);
$language_show = $language_configuration['language_show'];
}
// Without Language module menu links inherit the menu language and no
// language selector is shown.
else {
$default_langcode = ($menu_link->isNew() ? entity_load('menu', $menu_link->menu_name)->language()->getId() : $menu_link->langcode);
$language_show = FALSE;
}
$form['langcode'] = array(
'#type' => 'language_select',
'#title' => t('Language'),
'#languages' => LanguageInterface::STATE_ALL,
'#default_value' => $default_langcode,
'#access' => $language_show,
);
return parent::form($form, $form_state, $menu_link);
}
/**
* Overrides EntityForm::actions().
*/
protected function actions(array $form, array &$form_state) {
$element = parent::actions($form, $form_state);
$element['submit']['#button_type'] = 'primary';
return $element;
}
/**
* Overrides EntityForm::validate().
*/
public function validate(array $form, array &$form_state) {
$menu_link = $this->buildEntity($form, $form_state);
$normal_path = $this->pathAliasManager->getPathByAlias($menu_link->link_path);
if ($menu_link->link_path != $normal_path) {
drupal_set_message(t('The menu system stores system paths only, but will use the URL alias for display. %link_path has been stored as %normal_path', array('%link_path' => $menu_link->link_path, '%normal_path' => $normal_path)));
$menu_link->link_path = $normal_path;
$form_state['values']['link_path'] = $normal_path;
}
if (!UrlHelper::isExternal($menu_link->link_path)) {
$parsed_link = parse_url($menu_link->link_path);
if (isset($parsed_link['query'])) {
$menu_link->options['query'] = array();
parse_str($parsed_link['query'], $menu_link->options['query']);
}
else {
// Use unset() rather than setting to empty string
// to avoid redundant serialized data being stored.
unset($menu_link->options['query']);
}
if (isset($parsed_link['fragment'])) {
$menu_link->options['fragment'] = $parsed_link['fragment'];
}
else {
unset($menu_link->options['fragment']);
}
if (isset($parsed_link['path']) && $menu_link->link_path != $parsed_link['path']) {
$menu_link->link_path = $parsed_link['path'];
}
}
if (!trim($menu_link->link_path) || !drupal_valid_path($menu_link->link_path, TRUE)) {
$this->setFormError('link_path', $form_state, $this->t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $menu_link->link_path)));
}
parent::validate($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, array &$form_state) {
// @todo: Remove this when menu links are converted to content entities in
// http://drupal.org/node/1842858.
$entity = clone $this->entity;
// If you submit a form, the form state comes from caching, which forces
// the controller to be the one before caching. Ensure to have the
// controller of the current request.
$form_state['controller'] = $this;
// Copy top-level form values to entity properties, without changing
// existing entity properties that are not being edited by
// this form.
foreach ($form_state['values'] as $key => $value) {
$entity->$key = $value;
}
// Invoke all specified builders for copying form values to entity properties.
if (isset($form['#entity_builders'])) {
foreach ($form['#entity_builders'] as $function) {
call_user_func_array($function, array($entity->getEntityTypeId(), $entity, &$form, &$form_state));
}
}
return $entity;
}
/**
* Overrides EntityForm::submit().
*/
public function submit(array $form, array &$form_state) {
// Build the menu link object from the submitted values.
$menu_link = parent::submit($form, $form_state);
// The value of "hidden" is the opposite of the value supplied by the
// "enabled" checkbox.
$menu_link->hidden = (int) !$menu_link->enabled;
unset($menu_link->enabled);
$menu_link->options['attributes']['title'] = $menu_link->description;
list($menu_link->menu_name, $menu_link->plid) = explode(':', $menu_link->parent);
return $menu_link;
}
/**
* Overrides EntityForm::save().
*/
public function save(array $form, array &$form_state) {
$menu_link = $this->entity;
$saved = $menu_link->save();
if ($saved) {
drupal_set_message(t('The menu link has been saved.'));
$form_state['redirect_route'] = array(
'route_name' => 'menu_ui.menu_edit',
'route_parameters' => array(
'menu' => $menu_link->menu_name,
),
);
}
else {
drupal_set_message(t('There was an error saving the menu link.'), 'error');
$form_state['rebuild'] = TRUE;
}
}
}

View File

@ -1,45 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\menu_link\MenuLinkInterface.
*/
namespace Drupal\menu_link;
use Drupal\Core\Entity\EntityInterface;
use Symfony\Component\Routing\Route;
/**
* Provides an interface defining a menu link entity.
*/
interface MenuLinkInterface extends EntityInterface {
/**
* Returns the Route object associated with this link, if any.
*
* @return \Symfony\Component\Routing\Route|null
* The route object for this menu link, or NULL if there isn't one.
*/
public function getRoute();
/**
* Sets the route object for this link.
*
* This should only be called by MenuLinkStorage when loading
* the link object. Calling it at other times could result in unpredictable
* behavior.
*
* @param \Symfony\Component\Routing\Route $route
*/
public function setRouteObject(Route $route);
/**
* Resets a system-defined menu link.
*
* @return \Drupal\menu_link\MenuLinkInterface
* A menu link entity.
*/
public function reset();
}

View File

@ -1,319 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\menu_link\MenuLinkStorage.
*/
namespace Drupal\menu_link;
use Drupal\Core\Entity\EntityDatabaseStorage;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageException;
/**
* Controller class for menu links.
*
* This extends the Drupal\entity\EntityDatabaseStorage class, adding
* required special handling for menu_link entities.
*/
class MenuLinkStorage extends EntityDatabaseStorage implements MenuLinkStorageInterface {
/**
* Indicates whether the delete operation should re-parent children items.
*
* @var bool
*/
protected $preventReparenting = FALSE;
/**
* {@inheritdoc}
*/
public function create(array $values = array()) {
// The bundle of menu links being the menu name is not enforced but is the
// default behavior if no bundle is set.
if (!isset($values['bundle']) && isset($values['menu_name'])) {
$values['bundle'] = $values['menu_name'];
}
return parent::create($values);
}
/**
* {@inheritdoc}
*/
public function save(EntityInterface $entity) {
// We return SAVED_UPDATED by default because the logic below might not
// update the entity if its values haven't changed, so returning FALSE
// would be confusing in that situation.
$return = SAVED_UPDATED;
$transaction = $this->database->startTransaction();
try {
// Load the stored entity, if any.
if (!$entity->isNew() && !isset($entity->original)) {
$id = $entity->id();
if ($entity->getOriginalId() !== NULL) {
$id = $entity->getOriginalId();
}
$entity->original = $this->loadUnchanged($id);
}
if ($entity->isNew()) {
$entity->mlid = $this->database->insert($this->entityType->getBaseTable())->fields(array('menu_name' => $entity->menu_name))->execute();
$entity->enforceIsNew();
}
// Unlike the save() method from EntityDatabaseStorage, we invoke the
// 'presave' hook first because we want to allow modules to alter the
// entity before all the logic from our preSave() method.
$this->invokeHook('presave', $entity);
$entity->preSave($this);
// If every value in $entity->original is the same in the $entity, there
// is no reason to run the update queries or clear the caches. We use
// array_intersect_key() with the $entity as the first parameter because
// $entity may have additional keys left over from building a router entry.
// The intersect removes the extra keys, allowing a meaningful comparison.
if ($entity->isNew() || (array_intersect_key(get_object_vars($entity), get_object_vars($entity->original)) != get_object_vars($entity->original))) {
$return = drupal_write_record($this->entityType->getBaseTable(), $entity, $this->idKey);
if ($return) {
if (!$entity->isNew()) {
$this->resetCache(array($entity->{$this->idKey}));
$entity->postSave($this, TRUE);
$this->invokeHook('update', $entity);
}
else {
$return = SAVED_NEW;
$this->resetCache();
$entity->enforceIsNew(FALSE);
$entity->postSave($this, FALSE);
$this->invokeHook('insert', $entity);
}
}
}
// Ignore replica server temporarily.
db_ignore_replica();
unset($entity->original);
return $return;
}
catch (\Exception $e) {
$transaction->rollback();
watchdog_exception($this->entityTypeId, $e);
throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*/
public function setPreventReparenting($value = FALSE) {
$this->preventReparenting = $value;
}
/**
* {@inheritdoc}
*/
public function getPreventReparenting() {
return $this->preventReparenting;
}
/**
* {@inheritdoc}
*/
public function loadUpdatedCustomized(array $router_paths) {
$query = parent::buildQuery(NULL);
$query
->condition(db_or()
->condition('updated', 1)
->condition(db_and()
->condition('router_path', $router_paths, 'NOT IN')
->condition('external', 0)
->condition('customized', 1)
)
);
$query_result = $query->execute();
// We provide the necessary arguments for PDO to create objects of the
// specified entity class.
// @see \Drupal\Core\Entity\EntityInterface::__construct()
$query_result->setFetchMode(\PDO::FETCH_CLASS, $this->entityClass, array(array(), $this->entityTypeId));
return $query_result->fetchAllAssoc($this->idKey);
}
/**
* {@inheritdoc}
*/
public function loadModuleAdminTasks() {
// @todo - this code will move out of the menu link entity, so we are doing
// a straight SQL query for expediency.
$result = $this->database->select('menu_links');
$result->condition('machine_name', 'system.admin');
$result->addField('menu_links', 'mlid');
$plid = $result->execute()->fetchField();
$query = $this->database->select('menu_links', 'base', array('fetch' => \PDO::FETCH_ASSOC));
$query->fields('base');
$query
->condition('base.hidden', 0, '>=')
->condition('base.module', '', '>')
->condition('base.machine_name', '', '>')
->condition('base.p1', $plid);
$entities = $query->execute()->fetchAll();
return $entities;
}
/**
* {@inheritdoc}
*/
public function updateParentalStatus(EntityInterface $entity, $exclude = FALSE) {
// If plid == 0, there is nothing to update.
if ($entity->plid) {
// Check if at least one visible child exists in the table.
$query = $this->getQuery();
$query
->condition('menu_name', $entity->menu_name)
->condition('hidden', 0)
->condition('plid', $entity->plid)
->count();
if ($exclude) {
$query->condition('mlid', $entity->id(), '<>');
}
$parent_has_children = ((bool) $query->execute()) ? 1 : 0;
$this->database->update('menu_links')
->fields(array('has_children' => $parent_has_children))
->condition('mlid', $entity->plid)
->execute();
}
}
/**
* {@inheritdoc}
*/
public function findChildrenRelativeDepth(EntityInterface $entity) {
// @todo Since all we need is a specific field from the base table, does it
// make sense to convert to EFQ?
$query = $this->database->select('menu_links');
$query->addField('menu_links', 'depth');
$query->condition('menu_name', $entity->menu_name);
$query->orderBy('depth', 'DESC');
$query->range(0, 1);
$i = 1;
$p = 'p1';
while ($i <= MENU_MAX_DEPTH && $entity->{$p}) {
$query->condition($p, $entity->{$p});
$p = 'p' . ++$i;
}
$max_depth = $query->execute()->fetchField();
return ($max_depth > $entity->depth) ? $max_depth - $entity->depth : 0;
}
/**
* {@inheritdoc}
*/
public function moveChildren(EntityInterface $entity) {
$query = $this->database->update($this->entityType->getBaseTable());
$query->fields(array('menu_name' => $entity->menu_name));
$p = 'p1';
$expressions = array();
for ($i = 1; $i <= $entity->depth; $p = 'p' . ++$i) {
$expressions[] = array($p, ":p_$i", array(":p_$i" => $entity->{$p}));
}
$j = $entity->original->depth + 1;
while ($i <= MENU_MAX_DEPTH && $j <= MENU_MAX_DEPTH) {
$expressions[] = array('p' . $i++, 'p' . $j++, array());
}
while ($i <= MENU_MAX_DEPTH) {
$expressions[] = array('p' . $i++, 0, array());
}
$shift = $entity->depth - $entity->original->depth;
if ($shift > 0) {
// The order of expressions must be reversed so the new values don't
// overwrite the old ones before they can be used because "Single-table
// UPDATE assignments are generally evaluated from left to right"
// @see http://dev.mysql.com/doc/refman/5.0/en/update.html
$expressions = array_reverse($expressions);
}
foreach ($expressions as $expression) {
$query->expression($expression[0], $expression[1], $expression[2]);
}
$query->expression('depth', 'depth + :depth', array(':depth' => $shift));
$query->condition('menu_name', $entity->original->menu_name);
$p = 'p1';
for ($i = 1; $i <= MENU_MAX_DEPTH && $entity->original->{$p}; $p = 'p' . ++$i) {
$query->condition($p, $entity->original->{$p});
}
$query->execute();
// Check the has_children status of the parent, while excluding this item.
$this->updateParentalStatus($entity->original, TRUE);
}
/**
* {@inheritdoc}
*/
public function countMenuLinks($menu_name) {
$query = $this->getQuery();
$query
->condition('menu_name', $menu_name)
->count();
return $query->execute();
}
/**
* {@inheritdoc}
*/
public function getParentFromHierarchy(EntityInterface $entity) {
$parent_path = $entity->link_path;
do {
$parent = FALSE;
$parent_path = substr($parent_path, 0, strrpos($parent_path, '/'));
$query = $this->getQuery();
$query
->condition('mlid', $entity->id(), '<>')
->condition('module', 'system')
// We always respect the link's 'menu_name'; inheritance for router
// items is ensured in _menu_router_build().
->condition('menu_name', $entity->menu_name)
->condition('link_path', $parent_path);
$result = $query->execute();
// Only valid if we get a unique result.
if (count($result) == 1) {
$parent = $this->load(reset($result));
}
} while ($parent === FALSE && $parent_path);
return $parent;
}
/**
* {@inheritdoc}
*/
public function createFromDefaultLink(array $item) {
// Suggested items are disabled by default.
$item += array(
'hidden' => 0,
'options' => empty($item['description']) ? array() : array('attributes' => array('title' => $item['description'])),
);
return $this->create($item);
}
}

View File

@ -1,110 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\menu_link\MenuLinkStorageInterface.
*/
namespace Drupal\menu_link;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
/**
* Defines a common interface for menu link entity controller classes.
*/
interface MenuLinkStorageInterface extends EntityStorageInterface {
/**
* Sets an internal flag that allows us to prevent the reparenting operations
* executed during deletion.
*
* @param bool $value
* TRUE if reparenting should be allowed, FALSE if it should be prevented.
*/
public function setPreventReparenting($value = FALSE);
/**
* Gets value of internal flag that allows/prevents reparenting operations
* executed during deletion.
*
* @return bool
* TRUE if reparenting is allowed, FALSE if it is prevented.
*/
public function getPreventReparenting();
/**
* Loads system menu link as needed by system_get_module_admin_tasks().
*
* @return array
* An array of menu link entities indexed by their IDs.
*/
public function loadModuleAdminTasks();
/**
* Checks and updates the 'has_children' property for the parent of a link.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A menu link entity.
*/
public function updateParentalStatus(EntityInterface $entity, $exclude = FALSE);
/**
* Finds the depth of an item's children relative to its depth.
*
* For example, if the item has a depth of 2 and the maximum of any child in
* the menu link tree is 5, the relative depth is 3.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A menu link entity.
*
* @return int
* The relative depth, or zero.
*/
public function findChildrenRelativeDepth(EntityInterface $entity);
/**
* Updates the children of a menu link that is being moved.
*
* The menu name, parents (p1 - p6), and depth are updated for all children of
* the link, and the has_children status of the previous parent is updated.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A menu link entity.
*/
public function moveChildren(EntityInterface $entity);
/**
* Returns the number of menu links from a menu.
*
* @param string $menu_name
* The unique name of a menu.
*/
public function countMenuLinks($menu_name);
/**
* Tries to derive menu link's parent from the path hierarchy.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* A menu link entity.
*
* @return \Drupal\Core\Entity\EntityInterface|false
* A menu link entity or FALSE if not valid parent was found.
*/
public function getParentFromHierarchy(EntityInterface $entity);
/**
* Builds a menu link entity from a default item.
*
* This function should only be called for link data from
* the menu_link.static service.
*
* @param array $item
* An item returned from the menu_link.static service.
*
* @return \Drupal\menu_link\MenuLinkInterface
* A menu link entity.
*/
public function createFromDefaultLink(array $item);
}

View File

@ -1,619 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\menu_link\MenuTree.
*/
namespace Drupal\menu_link;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\Query\QueryFactory;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Provides the default implementation of a menu tree.
*/
class MenuTree implements MenuTreeInterface {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
* The database connection.
*/
protected $database;
/**
* The cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The menu link storage.
*
* @var \Drupal\menu_link\MenuLinkStorageInterface
*/
protected $menuLinkStorage;
/**
* The entity query factory.
*
* @var \Drupal\Core\Entity\Query\QueryFactory
*/
protected $queryFactory;
/**
* The state.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* A list of active trail paths keyed by $menu_name.
*
* @var array
*/
protected $trailPaths;
/**
* Stores the rendered menu output keyed by $menu_name.
*
* @var array
*/
protected $menuOutput;
/**
* Stores the menu tree used by the doBuildTree method, keyed by a cache ID.
*
* This cache ID is built using the $menu_name, the current language and
* some parameters passed into an entity query.
*/
protected $menuTree;
/**
* Stores the full menu tree data keyed by a cache ID.
*
* This variable distinct from static::$menuTree by having also items without
* access by the current user.
*
* This cache ID is built with the menu name, a passed in root link ID, the
* current language as well as the maximum depth.
*
* @var array
*/
protected $menuFullTrees;
/**
* Stores the menu tree data on the current page keyed by a cache ID.
*
* This contains less information than a tree built with buildAllData.
*
* @var array
*/
protected $menuPageTrees;
/**
* Constructs a new MenuTree.
*
* @param \Drupal\Core\Database\Connection $database
* The database connection.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* The cache backend.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Entity\Query\QueryFactory $entity_query_factory
* The entity query factory.
* @param \Drupal\Core\State\StateInterface $state
* The state.
*/
public function __construct(Connection $database, CacheBackendInterface $cache_backend, LanguageManagerInterface $language_manager, RequestStack $request_stack, EntityManagerInterface $entity_manager, QueryFactory $entity_query_factory, StateInterface $state) {
$this->database = $database;
$this->cache = $cache_backend;
$this->languageManager = $language_manager;
$this->requestStack = $request_stack;
$this->menuLinkStorage = $entity_manager->getStorage('menu_link');
$this->queryFactory = $entity_query_factory;
$this->state = $state;
}
/**
* {@inheritdoc}
*/
public function buildAllData($menu_name, $link = NULL, $max_depth = NULL) {
$language_interface = $this->languageManager->getCurrentLanguage();
// Use $mlid as a flag for whether the data being loaded is for the whole
// tree.
$mlid = isset($link['mlid']) ? $link['mlid'] : 0;
// Generate a cache ID (cid) specific for this $menu_name, $link, $language,
// and depth.
$cid = 'links:' . $menu_name . ':all:' . $mlid . ':' . $language_interface->id . ':' . (int) $max_depth;
if (!isset($this->menuFullTrees[$cid])) {
// If the static variable doesn't have the data, check {cache_menu}.
$cache = $this->cache->get($cid);
if ($cache && $cache->data) {
// If the cache entry exists, it contains the parameters for
// menu_build_tree().
$tree_parameters = $cache->data;
}
// If the tree data was not in the cache, build $tree_parameters.
if (!isset($tree_parameters)) {
$tree_parameters = array(
'min_depth' => 1,
'max_depth' => $max_depth,
);
if ($mlid) {
// The tree is for a single item, so we need to match the values in
// its p columns and 0 (the top level) with the plid values of other
// links.
$parents = array(0);
for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
if (!empty($link["p$i"])) {
$parents[] = $link["p$i"];
}
}
$tree_parameters['expanded'] = $parents;
$tree_parameters['active_trail'] = $parents;
$tree_parameters['active_trail'][] = $mlid;
}
// Cache the tree building parameters using the page-specific cid.
$this->cache->set($cid, $tree_parameters, Cache::PERMANENT, array('menu' => $menu_name));
}
// Build the tree using the parameters; the resulting tree will be cached
// by $this->doBuildTree()).
$this->menuFullTrees[$cid] = $this->buildTree($menu_name, $tree_parameters);
}
return $this->menuFullTrees[$cid];
}
/**
* {@inheritdoc}
*/
public function buildPageData($menu_name, $max_depth = NULL, $only_active_trail = FALSE) {
$language_interface = $this->languageManager->getCurrentLanguage();
// Load the request corresponding to the current page.
$request = $this->requestStack->getCurrentRequest();
$system_path = NULL;
if ($route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME)) {
// @todo https://drupal.org/node/2068471 is adding support so we can tell
// if this is called on a 404/403 page.
$system_path = $request->attributes->get('_system_path');
$page_not_403 = 1;
}
if (isset($system_path)) {
if (isset($max_depth)) {
$max_depth = min($max_depth, MENU_MAX_DEPTH);
}
// Generate a cache ID (cid) specific for this page.
$cid = 'links:' . $menu_name . ':page:' . $system_path . ':' . $language_interface->id . ':' . $page_not_403 . ':' . (int) $max_depth;
// If we are asked for the active trail only, and $menu_name has not been
// built and cached for this page yet, then this likely means that it
// won't be built anymore, as this function is invoked from
// template_preprocess_page(). So in order to not build a giant menu tree
// that needs to be checked for access on all levels, we simply check
// whether we have the menu already in cache, or otherwise, build a
// minimum tree containing the active trail only.
if (!isset($this->menuPageTrees[$cid]) && $only_active_trail) {
$cid .= ':trail';
}
if (!isset($this->menuPageTrees[$cid])) {
// If the static variable doesn't have the data, check {cache_menu}.
$cache = $this->cache->get($cid);
if ($cache && $cache->data) {
// If the cache entry exists, it contains the parameters for
// menu_build_tree().
$tree_parameters = $cache->data;
}
// If the tree data was not in the cache, build $tree_parameters.
if (!isset($tree_parameters)) {
$tree_parameters = array(
'min_depth' => 1,
'max_depth' => $max_depth,
);
$active_trail = $this->getActiveTrailIds($menu_name);
// If this page is accessible to the current user, build the tree
// parameters accordingly.
if ($page_not_403) {
// The active trail contains more than only array(0 => 0).
if (count($active_trail) > 1) {
// If we are asked to build links for the active trail only,skip
// the entire 'expanded' handling.
if ($only_active_trail) {
$tree_parameters['only_active_trail'] = TRUE;
}
}
$parents = $active_trail;
$expanded = $this->state->get('menu_expanded');
// Check whether the current menu has any links set to be expanded.
if (!$only_active_trail && $expanded && in_array($menu_name, $expanded)) {
// Collect all the links set to be expanded, and then add all of
// their children to the list as well.
do {
$query = $this->queryFactory->get('menu_link')
->condition('menu_name', $menu_name)
->condition('expanded', 1)
->condition('has_children', 1)
->condition('plid', $parents, 'IN')
->condition('mlid', $parents, 'NOT IN');
$result = $query->execute();
$parents += $result;
} while (!empty($result));
}
$tree_parameters['expanded'] = $parents;
$tree_parameters['active_trail'] = $active_trail;
}
// If access is denied, we only show top-level links in menus.
else {
$tree_parameters['expanded'] = $active_trail;
$tree_parameters['active_trail'] = $active_trail;
}
// Cache the tree building parameters using the page-specific cid.
$this->cache->set($cid, $tree_parameters, Cache::PERMANENT, array('menu' => $menu_name));
}
// Build the tree using the parameters; the resulting tree will be
// cached by $tihs->buildTree().
$this->menuPageTrees[$cid] = $this->buildTree($menu_name, $tree_parameters);
}
return $this->menuPageTrees[$cid];
}
return array();
}
/**
* {@inheritdoc}
*/
public function getActiveTrailIds($menu_name) {
// Parent mlids; used both as key and value to ensure uniqueness.
// We always want all the top-level links with plid == 0.
$active_trail = array(0 => 0);
$request = $this->requestStack->getCurrentRequest();
if ($route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME)) {
// @todo https://drupal.org/node/2068471 is adding support so we can tell
// if this is called on a 404/403 page.
// Check if the active trail has been overridden for this menu tree.
$active_path = $this->getPath($menu_name);
// Find a menu link corresponding to the current path. If
// $active_path is NULL, let menu_link_get_preferred() determine
// the path.
if ($active_link = $this->menuLinkGetPreferred($menu_name, $active_path)) {
if ($active_link['menu_name'] == $menu_name) {
// Use all the coordinates, except the last one because
// there can be no child beyond the last column.
for ($i = 1; $i < MENU_MAX_DEPTH; $i++) {
if ($active_link['p' . $i]) {
$active_trail[$active_link['p' . $i]] = $active_link['p' . $i];
}
}
}
}
}
return $active_trail;
}
/**
* {@inheritdoc}
*/
public function setPath($menu_name, $path = NULL) {
if (isset($path)) {
$this->trailPaths[$menu_name] = $path;
}
}
/**
* {@inheritdoc}
*/
public function getPath($menu_name) {
return isset($this->trailPaths[$menu_name]) ? $this->trailPaths[$menu_name] : NULL;
}
/**
* {@inheritdoc}
*/
public function renderMenu($menu_name) {
if (!isset($this->menuOutput[$menu_name])) {
$tree = $this->buildPageData($menu_name);
$this->menuOutput[$menu_name] = $this->renderTree($tree);
}
return $this->menuOutput[$menu_name];
}
/**
* {@inheritdoc}
*/
public function renderTree($tree) {
$build = array();
$items = array();
$menu_name = $tree ? end($tree)['link']['menu_name'] : '';
// Pull out just the menu links we are going to render so that we
// get an accurate count for the first/last classes.
foreach ($tree as $data) {
if ($data['link']['access'] && !$data['link']['hidden']) {
$items[] = $data;
}
}
foreach ($items as $data) {
$class = array();
// Set a class for the <li>-tag. Since $data['below'] may contain local
// tasks, only set 'expanded' class if the link also has children within
// the current menu.
if ($data['link']['has_children'] && $data['below']) {
$class[] = 'expanded';
}
elseif ($data['link']['has_children']) {
$class[] = 'collapsed';
}
else {
$class[] = 'leaf';
}
// Set a class if the link is in the active trail.
if ($data['link']['in_active_trail']) {
$class[] = 'active-trail';
$data['link']['localized_options']['attributes']['class'][] = 'active-trail';
}
// Allow menu-specific theme overrides.
$element['#theme'] = 'menu_link__' . strtr($data['link']['menu_name'], '-', '_');
$element['#attributes']['class'] = $class;
$element['#title'] = $data['link']['title'];
// @todo Use route name and parameters to generate the link path, unless
// it is external.
$element['#href'] = $data['link']['link_path'];
$element['#localized_options'] = !empty($data['link']['localized_options']) ? $data['link']['localized_options'] : array();
$element['#below'] = $data['below'] ? $this->renderTree($data['below']) : $data['below'];
$element['#original_link'] = $data['link'];
// Index using the link's unique mlid.
$build[$data['link']['mlid']] = $element;
}
if ($build) {
// Make sure drupal_render() does not re-order the links.
$build['#sorted'] = TRUE;
// Add the theme wrapper for outer markup.
// Allow menu-specific theme overrides.
$build['#theme_wrappers'][] = 'menu_tree__' . strtr($menu_name, '-', '_');
// Set cache tag.
$menu_name = $data['link']['menu_name'];
$build['#cache']['tags']['menu'][$menu_name] = $menu_name;
}
return $build;
}
/**
* {@inheritdoc}
*/
public function buildTree($menu_name, array $parameters = array()) {
// Build the menu tree.
$tree = $this->doBuildTree($menu_name, $parameters);
// Check access for the current user to each item in the tree.
$this->checkAccess($tree);
return $tree;
}
/**
* Builds a menu tree.
*
* This function may be used build the data for a menu tree only, for example
* to further massage the data manually before further processing happens.
* MenuTree::checkAccess() needs to be invoked afterwards.
*
* @param string $menu_name
* The name of the menu.
* @param array $parameters
* The parameters passed into static::buildTree()
*
* @see static::buildTree()
*/
protected function doBuildTree($menu_name, array $parameters = array()) {
$language_interface = $this->languageManager->getCurrentLanguage();
// Build the cache id; sort parents to prevent duplicate storage and remove
// default parameter values.
if (isset($parameters['expanded'])) {
sort($parameters['expanded']);
}
$tree_cid = 'links:' . $menu_name . ':tree-data:' . $language_interface->id . ':' . hash('sha256', serialize($parameters));
// If we do not have this tree in the static cache, check {cache_menu}.
if (!isset($this->menuTree[$tree_cid])) {
$cache = $this->cache->get($tree_cid);
if ($cache && $cache->data) {
$this->menuTree[$tree_cid] = $cache->data;
}
}
if (!isset($this->menuTree[$tree_cid])) {
$query = $this->queryFactory->get('menu_link');
for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) {
$query->sort('p' . $i, 'ASC');
}
$query->condition('menu_name', $menu_name);
if (!empty($parameters['expanded'])) {
$query->condition('plid', $parameters['expanded'], 'IN');
}
elseif (!empty($parameters['only_active_trail'])) {
$query->condition('mlid', $parameters['active_trail'], 'IN');
}
$min_depth = (isset($parameters['min_depth']) ? $parameters['min_depth'] : 1);
if ($min_depth != 1) {
$query->condition('depth', $min_depth, '>=');
}
if (isset($parameters['max_depth'])) {
$query->condition('depth', $parameters['max_depth'], '<=');
}
// Add custom query conditions, if any were passed.
if (isset($parameters['conditions'])) {
foreach ($parameters['conditions'] as $column => $value) {
$query->condition($column, $value);
}
}
// Build an ordered array of links using the query result object.
$links = array();
if ($result = $query->execute()) {
$links = $this->menuLinkStorage->loadMultiple($result);
}
$active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array());
$tree = $this->doBuildTreeData($links, $active_trail, $min_depth);
// Cache the data, if it is not already in the cache.
$this->cache->set($tree_cid, $tree, Cache::PERMANENT, array('menu' => $menu_name));
$this->menuTree[$tree_cid] = $tree;
}
return $this->menuTree[$tree_cid];
}
/**
* Sorts the menu tree and recursively checks access for each item.
*
* @param array $tree
* The menu tree you wish to operate on.
*/
protected function checkAccess(&$tree) {
$new_tree = array();
foreach ($tree as $key => $v) {
$item = &$tree[$key]['link'];
$this->menuLinkTranslate($item);
if ($item['access'] || ($item['in_active_trail'] && strpos($item['href'], '%') !== FALSE)) {
if ($tree[$key]['below']) {
$this->checkAccess($tree[$key]['below']);
}
// The weights are made a uniform 5 digits by adding 50000 as an offset.
// After _menu_link_translate(), $item['title'] has the localized link
// title. Adding the mlid to the end of the index insures that it is
// unique.
$new_tree[(50000 + $item['weight']) . ' ' . $item['title'] . ' ' . $item['mlid']] = $tree[$key];
}
}
// Sort siblings in the tree based on the weights and localized titles.
ksort($new_tree);
$tree = $new_tree;
}
/**
* {@inheritdoc}
*/
public function buildTreeData(array $links, array $parents = array(), $depth = 1) {
$tree = $this->doBuildTreeData($links, $parents, $depth);
$this->checkAccess($tree);
return $tree;
}
/**
* Prepares the data for calling $this->treeDataRecursive().
*/
protected function doBuildTreeData(array $links, array $parents = array(), $depth = 1) {
// Reverse the array so we can use the more efficient array_pop() function.
$links = array_reverse($links);
return $this->treeDataRecursive($links, $parents, $depth);
}
/**
* Builds the data representing a menu tree.
*
* The function is a bit complex because the rendering of a link depends on
* the next menu link.
*
* @param array $links
* A flat array of menu links that are part of the menu. Each array element
* is an associative array of information about the menu link, containing
* the fields from the {menu_links} table, and optionally additional
* information from the {menu_router} table, if the menu item appears in
* both tables. This array must be ordered depth-first.
* See _menu_build_tree() for a sample query.
* @param array $parents
* An array of the menu link ID values that are in the path from the current
* page to the root of the menu tree.
* @param int $depth
* The minimum depth to include in the returned menu tree.
*
* @return array
*/
protected function treeDataRecursive(&$links, $parents, $depth) {
$tree = array();
while ($item = array_pop($links)) {
// We need to determine if we're on the path to root so we can later build
// the correct active trail.
$item['in_active_trail'] = in_array($item['mlid'], $parents);
// Add the current link to the tree.
$tree[$item['mlid']] = array(
'link' => $item,
'below' => array(),
);
// Look ahead to the next link, but leave it on the array so it's
// available to other recursive function calls if we return or build a
// sub-tree.
$next = end($links);
// Check whether the next link is the first in a new sub-tree.
if ($next && $next['depth'] > $depth) {
// Recursively call doBuildTreeData to build the sub-tree.
$tree[$item['mlid']]['below'] = $this->treeDataRecursive($links, $parents, $next['depth']);
// Fetch next link after filling the sub-tree.
$next = end($links);
}
// Determine if we should exit the loop and return.
if (!$next || $next['depth'] < $depth) {
break;
}
}
return $tree;
}
/**
* Wraps menu_link_get_preferred().
*/
protected function menuLinkGetPreferred($menu_name, $active_path) {
return menu_link_get_preferred($active_path, $menu_name);
}
/**
* Wraps _menu_link_translate().
*/
protected function menuLinkTranslate(&$item) {
_menu_link_translate($item);
}
}

View File

@ -1,182 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\menu_link\MenuTreeInterface.
*/
namespace Drupal\menu_link;
/**
* Defines an interface for trees out of menu links.
*/
interface MenuTreeInterface {
/**
* Returns a rendered menu tree.
*
* The menu item's LI element is given one of the following classes:
* - expanded: The menu item is showing its submenu.
* - collapsed: The menu item has a submenu which is not shown.
* - leaf: The menu item has no submenu.
*
* @param array $tree
* A data structure representing the tree as returned from menu_tree_data.
*
* @return array
* A structured array to be rendered by drupal_render().
*/
public function renderTree($tree);
/**
* Sets the path for determining the active trail of the specified menu tree.
*
* This path will also affect the breadcrumbs under some circumstances.
* Breadcrumbs are built using the preferred link returned by
* menu_link_get_preferred(). If the preferred link is inside one of the menus
* specified in calls to static::setPath(), the preferred link will be
* overridden by the corresponding path returned by static::getPath().
*
* @param string $menu_name
* The name of the affected menu tree.
* @param string $path
* The path to use when finding the active trail.
*/
public function setPath($menu_name, $path = NULL);
/**
* Gets the path for determining the active trail of the specified menu tree.
*
* @param string $menu_name
* The menu name of the requested tree.
*
* @return string
* A string containing the path. If no path has been specified with
* static::setPath(), NULL is returned.
*/
public function getPath($menu_name);
/**
* Gets the active trail IDs of the specified menu tree.
*
* @param string $menu_name
* The menu name of the requested tree.
*
* @return array
* An array containing the active trail: a list of mlids.
*/
public function getActiveTrailIds($menu_name);
/**
* Sorts and returns the built data representing a menu tree.
*
* @param array $links
* A flat array of menu links that are part of the menu. Each array element
* is an associative array of information about the menu link, containing
* the fields from the {menu_links} table, and optionally additional
* information from the {menu_router} table, if the menu item appears in
* both tables. This array must be ordered depth-first.
* See _menu_build_tree() for a sample query.
* @param array $parents
* An array of the menu link ID values that are in the path from the current
* page to the root of the menu tree.
* @param int $depth
* The minimum depth to include in the returned menu tree.
*
* @return array
* An array of menu links in the form of a tree. Each item in the tree is an
* associative array containing:
* - link: The menu link item from $links, with additional element
* 'in_active_trail' (TRUE if the link ID was in $parents).
* - below: An array containing the sub-tree of this item, where each
* element is a tree item array with 'link' and 'below' elements. This
* array will be empty if the menu item has no items in its sub-tree
* having a depth greater than or equal to $depth.
*/
public function buildTreeData(array $links, array $parents = array(), $depth = 1);
/**
* Gets the data structure for a named menu tree, based on the current page.
*
* The tree order is maintained by storing each parent in an individual
* field, see http://drupal.org/node/141866 for more.
*
* @param string $menu_name
* The named menu links to return.
* @param int $max_depth
* (optional) The maximum depth of links to retrieve.
* @param bool $only_active_trail
* (optional) Whether to only return the links in the active trail (TRUE)
* instead of all links on every level of the menu link tree (FALSE).
* Defaults to FALSE.
*
* @return array
* An array of menu links, in the order they should be rendered. The array
* is a list of associative arrays -- these have two keys, link and below.
* link is a menu item, ready for theming as a link. Below represents the
* submenu below the link if there is one, and it is a subtree that has the
* same structure described for the top-level array.
*/
public function buildPageData($menu_name, $max_depth = NULL, $only_active_trail = FALSE);
/**
* Gets the data structure representing a named menu tree.
*
* Since this can be the full tree including hidden items, the data returned
* may be used for generating an an admin interface or a select.
*
* @param string $menu_name
* The named menu links to return
* @param array $link
* A fully loaded menu link, or NULL. If a link is supplied, only the
* path to root will be included in the returned tree - as if this link
* represented the current page in a visible menu.
* @param int $max_depth
* Optional maximum depth of links to retrieve. Typically useful if only one
* or two levels of a sub tree are needed in conjunction with a non-NULL
* $link, in which case $max_depth should be greater than $link['depth'].
*
* @return array
* An tree of menu links in an array, in the order they should be rendered.
*/
public function buildAllData($menu_name, $link = NULL, $max_depth = NULL);
/**
* Renders a menu tree based on the current path.
*
* @param string $menu_name
* The name of the menu.
*
* @return array
* A structured array representing the specified menu on the current page,
* to be rendered by drupal_render().
*/
public function renderMenu($menu_name);
/**
* Builds a menu tree, translates links, and checks access.
*
* @param string $menu_name
* The name of the menu.
* @param array $parameters
* (optional) An associative array of build parameters. Possible keys:
* - expanded: An array of parent link ids to return only menu links that
* are children of one of the plids in this list. If empty, the whole menu
* tree is built, unless 'only_active_trail' is TRUE.
* - active_trail: An array of mlids, representing the coordinates of the
* currently active menu link.
* - only_active_trail: Whether to only return links that are in the active
* trail. This option is ignored, if 'expanded' is non-empty.
* - min_depth: The minimum depth of menu links in the resulting tree.
* Defaults to 1, which is the default to build a whole tree for a menu
* (excluding menu container itself).
* - max_depth: The maximum depth of menu links in the resulting tree.
* - conditions: An associative array of custom database select query
* condition key/value pairs; see _menu_build_tree() for the actual query.
*
* @return array
* A fully built menu tree.
*/
public function buildTree($menu_name, array $parameters = array());
}

View File

@ -1,76 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\Core\Menu\StaticMenuLinks.
*/
namespace Drupal\menu_link;
use Drupal\Component\Discovery\YamlDiscovery;
use Drupal\Core\Extension\ModuleHandlerInterface;
/**
* Provides a service which finds and alters default menu links in yml files.
*/
class StaticMenuLinks {
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructs a new StaticMenuLinks.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct(ModuleHandlerInterface $module_handler) {
$this->moduleHandler = $module_handler;
}
/**
* Gets the menu links defined in YAML files.
*
* @return array
* An array of default menu links.
*/
public function getLinks() {
$discovery = $this->getDiscovery();
foreach ($discovery->findAll() as $module => $menu_links) {
foreach ($menu_links as $machine_name => $menu_link) {
$all_links[$machine_name] = $menu_link;
$all_links[$machine_name]['machine_name'] = $machine_name;
$all_links[$machine_name]['module'] = $module;
}
}
$this->moduleHandler->alter('menu_link_defaults', $all_links);
foreach ($all_links as $machine_name => $menu_link) {
// Set the machine_name to the menu links added dynamically.
if (!isset($menu_link['machine_name'])) {
$all_links[$machine_name]['machine_name'] = $machine_name;
}
// Change the key to match the DB column for now.
$all_links[$machine_name]['link_title'] = $all_links[$machine_name]['title'];
unset($all_links[$machine_name]['title']);
}
return $all_links;
}
/**
* Creates a YAML discovery for menu links.
*
* @return \Drupal\Component\Discovery\YamlDiscovery
* An YAML discovery instance.
*/
protected function getDiscovery() {
return new YamlDiscovery('links.menu', $this->moduleHandler->getModuleDirectories());
}
}

View File

@ -1,539 +0,0 @@
<?php
/**
* @file
* Contains \Drupal\menu_link\Tests\MenuTreeTest.
*/
namespace Drupal\menu_link\Tests {
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Language\Language;
use Drupal\menu_link\MenuTree;
use Drupal\Tests\UnitTestCase;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
if (!defined('MENU_MAX_DEPTH')) {
define('MENU_MAX_DEPTH', 9);
}
/**
* @coversDefaultClass \Drupal\menu_link\MenuTree
* @group menu_link
*/
class MenuTreeTest extends UnitTestCase {
/**
* The tested menu tree.
*
* @var \Drupal\menu_link\MenuTree|\Drupal\menu_link\Tests\TestMenuTree
*/
protected $menuTree;
/**
* The mocked database connection.
*
* @var \Drupal\Core\DatabaseConnection|\PHPUnit_Framework_MockObject_MockObject
*/
protected $connection;
/**
* The mocked cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $cacheBackend;
/**
* The mocked language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $languageManager;
/**
* The test request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack.
*/
protected $requestStack;
/**
* The mocked entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $entityManager;
/**
* The mocked entity query factor.y
*
* @var \Drupal\Core\Entity\Query\QueryFactory|\PHPUnit_Framework_MockObject_MockObject
*/
protected $entityQueryFactory;
/**
* The mocked state.
*
* @var \Drupal\Core\State\StateInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $state;
/**
* Stores some default values for a menu link.
*
* @var array
*/
protected $defaultMenuLink = array(
'menu_name' => 'main-menu',
'mlid' => 1,
'title' => 'Example 1',
'route_name' => 'example1',
'link_path' => 'example1',
'access' => 1,
'hidden' => FALSE,
'has_children' => FALSE,
'in_active_trail' => TRUE,
'localized_options' => array('attributes' => array('title' => '')),
'weight' => 0,
);
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
->disableOriginalConstructor()
->getMock();
$this->cacheBackend = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
$this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface');
$this->requestStack = new RequestStack();
$this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
$this->entityQueryFactory = $this->getMockBuilder('Drupal\Core\Entity\Query\QueryFactory')
->disableOriginalConstructor()
->getMock();
$this->state = $this->getMock('Drupal\Core\State\StateInterface');
$this->menuTree = new TestMenuTree($this->connection, $this->cacheBackend, $this->languageManager, $this->requestStack, $this->entityManager, $this->entityQueryFactory, $this->state);
}
/**
* Tests active paths.
*
* @covers ::setPath
* @covers ::getPath
*/
public function testActivePaths() {
$this->assertNull($this->menuTree->getPath('test_menu1'));
$this->menuTree->setPath('test_menu1', 'example_path1');
$this->assertEquals('example_path1', $this->menuTree->getPath('test_menu1'));
$this->assertNull($this->menuTree->getPath('test_menu2'));
$this->menuTree->setPath('test_menu2', 'example_path2');
$this->assertEquals('example_path1', $this->menuTree->getPath('test_menu1'));
$this->assertEquals('example_path2', $this->menuTree->getPath('test_menu2'));
}
/**
* Tests buildTreeData with a single level.
*
* @covers ::buildTreeData
* @covers ::doBuildTreeData
*/
public function testBuildTreeDataWithSingleLevel() {
$items = array();
$items[] = array(
'mlid' => 1,
'depth' => 1,
'weight' => 0,
'title' => '',
'route_name' => 'example1',
'access' => TRUE,
);
$items[] = array(
'mlid' => 2,
'depth' => 1,
'weight' => 0,
'title' => '',
'route_name' => 'example2',
'access' => TRUE,
);
$result = $this->menuTree->buildTreeData($items, array(), 1);
$this->assertCount(2, $result);
$result1 = array_shift($result);
$this->assertEquals($items[0] + array('in_active_trail' => FALSE), $result1['link']);
$result2 = array_shift($result);
$this->assertEquals($items[1] + array('in_active_trail' => FALSE), $result2['link']);
}
/**
* Tests buildTreeData with a single level and one item being active.
*
* @covers ::buildTreeData
* @covers ::doBuildTreeData
*/
public function testBuildTreeDataWithSingleLevelAndActiveItem() {
$items = array();
$items[] = array(
'mlid' => 1,
'depth' => 1,
'weight' => 0,
'title' => '',
'route_name' => 'example1',
'access' => TRUE,
);
$items[] = array(
'mlid' => 2,
'depth' => 1,
'weight' => 0,
'title' => '',
'route_name' => 'example2',
'access' => TRUE,
);
$result = $this->menuTree->buildTreeData($items, array(1), 1);
$this->assertCount(2, $result);
$result1 = array_shift($result);
$this->assertEquals($items[0] + array('in_active_trail' => TRUE), $result1['link']);
$result2 = array_shift($result);
$this->assertEquals($items[1] + array('in_active_trail' => FALSE), $result2['link']);
}
/**
* Tests buildTreeData with a single level and none item being active.
*
* @covers ::buildTreeData
* @covers ::doBuildTreeData
*/
public function testBuildTreeDataWithSingleLevelAndNoActiveItem() {
$items = array();
$items[] = array(
'mlid' => 1,
'depth' => 1,
'weight' => 0,
'title' => '',
'route_name' => 'example1',
'access' => TRUE,
);
$items[] = array(
'mlid' => 2,
'depth' => 1,
'weight' => 0,
'title' => '',
'route_name' => 'example2',
'access' => TRUE,
);
$result = $this->menuTree->buildTreeData($items, array(3), 1);
$this->assertCount(2, $result);
$result1 = array_shift($result);
$this->assertEquals($items[0] + array('in_active_trail' => FALSE), $result1['link']);
$result2 = array_shift($result);
$this->assertEquals($items[1] + array('in_active_trail' => FALSE), $result2['link']);
}
/**
* Tests buildTreeData with a more complex example.
*
* @covers ::buildTreeData
* @covers ::doBuildTreeData
*/
public function testBuildTreeWithComplexData() {
$items = array(
1 => array('mlid' => 1, 'depth' => 1, 'route_name' => 'example1', 'access' => TRUE, 'weight' => 0, 'title' => ''),
2 => array('mlid' => 2, 'depth' => 1, 'route_name' => 'example2', 'access' => TRUE, 'weight' => 0, 'title' => ''),
3 => array('mlid' => 3, 'depth' => 2, 'route_name' => 'example3', 'access' => TRUE, 'weight' => 0, 'title' => ''),
4 => array('mlid' => 4, 'depth' => 3, 'route_name' => 'example4', 'access' => TRUE, 'weight' => 0, 'title' => ''),
5 => array('mlid' => 5, 'depth' => 1, 'route_name' => 'example5', 'access' => TRUE, 'weight' => 0, 'title' => ''),
);
$tree = $this->menuTree->buildTreeData($items);
// Validate that parent items #1, #2, and #5 exist on the root level.
$this->assertEquals($items[1]['mlid'], $tree['50000 1']['link']['mlid']);
$this->assertEquals($items[2]['mlid'], $tree['50000 2']['link']['mlid']);
$this->assertEquals($items[5]['mlid'], $tree['50000 5']['link']['mlid']);
// Validate that child item #4 exists at the correct location in the hierarchy.
$this->assertEquals($items[4]['mlid'], $tree['50000 2']['below']['50000 3']['below']['50000 4']['link']['mlid']);
}
/**
* Tests getActiveTrailIds().
*
* @covers ::getActiveTrailIds()
*/
public function testGetActiveTrailIds() {
$menu_link = array(
'mlid' => 10,
'route_name' => 'example1',
'p1' => 3,
'p2' => 2,
'p3' => 1,
'p4' => 4,
'p5' => 9,
'p6' => 5,
'p7' => 6,
'p8' => 7,
'p9' => 8,
'menu_name' => 'test_menu'
);
$this->menuTree->setPreferredMenuLink('test_menu', 'test/path', $menu_link);
$request = (new Request());
$request->attributes->set(RouteObjectInterface::ROUTE_NAME, 'test_route');
$this->requestStack->push($request);
$this->menuTree->setPath('test_menu', 'test/path');
$trail = $this->menuTree->getActiveTrailIds('test_menu');
$this->assertEquals(array(0 => 0, 3 => 3, 2 => 2, 1 => 1, 4 => 4, 9 => 9, 5 => 5, 6 => 6, 7 => 7), $trail);
}
/**
* Tests getActiveTrailIds() without preferred link.
*
* @covers ::getActiveTrailIds()
*/
public function testGetActiveTrailIdsWithoutPreferredLink() {
$request = (new Request());
$request->attributes->set(RouteObjectInterface::ROUTE_NAME, 'test_route');
$this->requestStack->push($request);
$this->menuTree->setPath('test_menu', 'test/path');
$trail = $this->menuTree->getActiveTrailIds('test_menu');
$this->assertEquals(array(0 => 0), $trail);
}
/**
* Tests buildTree with simple menu_name and no parameters.
*/
public function testBuildTreeWithoutParameters() {
$language = new Language(array('id' => 'en'));
$this->languageManager->expects($this->any())
->method('getCurrentLanguage')
->will($this->returnValue($language));
// Setup query and the query result.
$query = $this->getMock('Drupal\Core\Entity\Query\QueryInterface');
$this->entityQueryFactory->expects($this->once())
->method('get')
->with('menu_link')
->will($this->returnValue($query));
$query->expects($this->once())
->method('condition')
->with('menu_name', 'test_menu');
$query->expects($this->once())
->method('execute')
->will($this->returnValue(array(1, 2, 3)));
$storage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface');
$base = array(
'access' => TRUE,
'weight' => 0,
'title' => 'title',
);
$menu_link = $base + array(
'mlid' => 1,
'p1' => 3,
'p2' => 2,
'p3' => 1,
);
$links[1] = $menu_link;
$menu_link = $base + array(
'mlid' => 3,
'p1' => 3,
'depth' => 1,
);
$links[3] = $menu_link;
$menu_link = $base + array(
'mlid' => 2,
'p1' => 3,
'p2' => 2,
'depth' => 2,
);
$links[2] = $menu_link;
$storage->expects($this->once())
->method('loadMultiple')
->with(array(1, 2, 3))
->will($this->returnValue($links));
$this->menuTree->setStorage($storage);
// Ensure that static/non static caching works.
// First setup no working caching.
$this->cacheBackend->expects($this->at(0))
->method('get')
->with('links:test_menu:tree-data:en:35786c7117b4e38d0f169239752ce71158266ae2f6e4aa230fbbb87bd699c0e3')
->will($this->returnValue(FALSE));
$this->cacheBackend->expects($this->at(1))
->method('set')
->with('links:test_menu:tree-data:en:35786c7117b4e38d0f169239752ce71158266ae2f6e4aa230fbbb87bd699c0e3', $this->anything(), Cache::PERMANENT, array('menu' => 'test_menu'));
// Ensure that the static caching triggered.
$this->cacheBackend->expects($this->exactly(1))
->method('get');
$this->menuTree->buildTree('test_menu');
$this->menuTree->buildTree('test_menu');
}
/**
* Tests the output with a single level.
*
* @covers ::renderTree
*/
public function testOutputWithSingleLevel() {
$tree = array(
'1' => array(
'link' => array('mlid' => 1) + $this->defaultMenuLink,
'below' => array(),
),
'2' => array(
'link' => array('mlid' => 2) + $this->defaultMenuLink,
'below' => array(),
),
);
$output = $this->menuTree->renderTree($tree);
// Validate that the - in main-menu is changed into an underscore
$this->assertEquals($output['1']['#theme'], 'menu_link__main_menu', 'Hyphen is changed to an underscore on menu_link');
$this->assertEquals($output['2']['#theme'], 'menu_link__main_menu', 'Hyphen is changed to an underscore on menu_link');
$this->assertEquals($output['#theme_wrappers'][0], 'menu_tree__main_menu', 'Hyphen is changed to an underscore on menu_tree wrapper');
}
/**
* Tests the output method with a complex example.
*
* @covers ::renderTree
*/
public function testOutputWithComplexData() {
$tree = array(
'1'=> array(
'link' => array('mlid' => 1, 'has_children' => 1, 'title' => 'Item 1', 'link_path' => 'a') + $this->defaultMenuLink,
'below' => array(
'2' => array('link' => array('mlid' => 2, 'title' => 'Item 2', 'link_path' => 'a/b') + $this->defaultMenuLink,
'below' => array(
'3' => array('link' => array('mlid' => 3, 'title' => 'Item 3', 'in_active_trail' => 0, 'link_path' => 'a/b/c') + $this->defaultMenuLink,
'below' => array()),
'4' => array('link' => array('mlid' => 4, 'title' => 'Item 4', 'in_active_trail' => 0, 'link_path' => 'a/b/d') + $this->defaultMenuLink,
'below' => array())
)
)
)
),
'5' => array('link' => array('mlid' => 5, 'hidden' => 1, 'title' => 'Item 5', 'link_path' => 'e') + $this->defaultMenuLink, 'below' => array()),
'6' => array('link' => array('mlid' => 6, 'title' => 'Item 6', 'in_active_trail' => 0, 'access' => 0, 'link_path' => 'f') + $this->defaultMenuLink, 'below' => array()),
'7' => array('link' => array('mlid' => 7, 'title' => 'Item 7', 'in_active_trail' => 0, 'link_path' => 'g') + $this->defaultMenuLink, 'below' => array())
);
$output = $this->menuTree->renderTree($tree);
// Looking for child items in the data
$this->assertEquals( $output['1']['#below']['2']['#href'], 'a/b', 'Checking the href on a child item');
$this->assertTrue(in_array('active-trail', $output['1']['#below']['2']['#attributes']['class']), 'Checking the active trail class');
// Validate that the hidden and no access items are missing
$this->assertFalse(isset($output['5']), 'Hidden item should be missing');
$this->assertFalse(isset($output['6']), 'False access should be missing');
// Item 7 is after a couple hidden items. Just to make sure that 5 and 6 are
// skipped and 7 still included.
$this->assertTrue(isset($output['7']), 'Item after hidden items is present');
}
/**
* Tests menu tree access check with a single level.
*
* @covers ::checkAccess
*/
public function testCheckAccessWithSingleLevel() {
$items = array(
array('mlid' => 1, 'route_name' => 'menu_test_1', 'depth' => 1, 'link_path' => 'menu_test/test_1', 'in_active_trail' => FALSE) + $this->defaultMenuLink,
array('mlid' => 2, 'route_name' => 'menu_test_2', 'depth' => 1, 'link_path' => 'menu_test/test_2', 'in_active_trail' => FALSE) + $this->defaultMenuLink,
);
// Register a menuLinkTranslate to mock the access.
$this->menuTree->menuLinkTranslateCallable = function(&$item) {
$item['access'] = $item['mlid'] == 1;
};
// Build the menu tree and check access for all of the items.
$tree = $this->menuTree->buildTreeData($items);
$this->assertCount(1, $tree);
$item = reset($tree);
$this->assertEquals($items[0], $item['link']);
}
}
class TestMenuTree extends MenuTree {
/**
* An alternative callable used for menuLinkTranslate.
* @var callable
*/
public $menuLinkTranslateCallable;
/**
* Stores the preferred menu link per menu and path.
*
* @var array
*/
protected $preferredMenuLink;
/**
* {@inheritdoc}
*/
protected function menuLinkTranslate(&$item) {
if (isset($this->menuLinkTranslateCallable)) {
call_user_func_array($this->menuLinkTranslateCallable, array(&$item));
}
}
/**
* {@inheritdoc}
*/
protected function menuLinkGetPreferred($menu_name, $active_path) {
return isset($this->preferredMenuLink[$menu_name][$active_path]) ? $this->preferredMenuLink[$menu_name][$active_path] : NULL;
}
/**
* Set the storage.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The menu link storage.
*/
public function setStorage(EntityStorageInterface $storage) {
$this->menuLinkStorage = $storage;
}
/**
* Sets the preferred menu link.
*
* @param string $menu_name
* The menu name.
* @param string $active_path
* The active path.
* @param array $menu_link
* The preferred menu link.
*/
public function setPreferredMenuLink($menu_name, $active_path, $menu_link) {
$this->preferredMenuLink[$menu_name][$active_path] = $menu_link;
}
}
}
namespace {
if (!defined('MENU_MAX_DEPTH')) {
define('MENU_MAX_DEPTH', 9);
}
}

View File

@ -1,66 +0,0 @@
<?php
/**
* @file
* Definition of Drupal\views\Plugin\views\sort\MenuHierarchy.
*/
namespace Drupal\views\Plugin\views\sort;
use Drupal\views\Views;
/**
* Sort in menu hierarchy order.
*
* Given a field name of 'p' this produces an ORDER BY on p1, p2, ..., p9;
* and optionally injects multiple joins to {menu_links} to sort by weight
* and title as well.
*
* This is only really useful for the {menu_links} table.
*
* @ViewsSort("menu_hierarchy")
*/
class MenuHierarchy extends SortPluginBase {
protected function defineOptions() {
$options = parent::defineOptions();
$options['sort_within_level'] = array('default' => FALSE);
return $options;
}
public function buildOptionsForm(&$form, &$form_state) {
parent::buildOptionsForm($form, $form_state);
$form['sort_within_level'] = array(
'#type' => 'checkbox',
'#title' => t('Sort within each hierarchy level'),
'#description' => t('Enable this to sort the items within each level of the hierarchy by weight and title. Warning: this may produce a slow query.'),
'#default_value' => $this->options['sort_within_level'],
);
}
public function query() {
$this->ensureMyTable();
$max_depth = isset($this->definition['max depth']) ? $this->definition['max depth'] : MENU_MAX_DEPTH;
for ($i = 1; $i <= $max_depth; ++$i) {
if ($this->options['sort_within_level']) {
$definition = array(
'table' => 'menu_links',
'field' => 'mlid',
'left_table' => $this->tableAlias,
'left_field' => $this->field . $i
);
$join = Views::pluginManager('join')->createInstance('standard', $definition);
$menu_links = $this->query->addTable('menu_links', NULL, $join);
$this->query->addOrderBy($menu_links, 'weight', $this->options['order']);
$this->query->addOrderBy($menu_links, 'link_title', $this->options['order']);
}
// We need this even when also sorting by weight and title, to make sure
// that children of two parents with the same weight and title are
// correctly separated.
$this->query->addOrderBy($this->tableAlias, $this->field . $i, $this->options['order']);
}
}
}