MenuForm.php in Zircon Profile 8
Same filename and directory in other branches
Contains \Drupal\menu_ui\MenuForm.
Namespace
Drupal\menu_uiFile
core/modules/menu_ui/src/MenuForm.phpView source
<?php
/**
* @file
* Contains \Drupal\menu_ui\MenuForm.
*/
namespace Drupal\menu_ui;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\Query\QueryFactory;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Link;
use Drupal\Core\Menu\MenuLinkManagerInterface;
use Drupal\Core\Menu\MenuLinkTreeElement;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
use Drupal\Core\Utility\LinkGeneratorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base form for menu edit forms.
*/
class MenuForm extends EntityForm {
/**
* The factory for entity queries.
*
* @var \Drupal\Core\Entity\Query\QueryFactory
*/
protected $entityQueryFactory;
/**
* The menu link manager.
*
* @var \Drupal\Core\Menu\MenuLinkManagerInterface
*/
protected $menuLinkManager;
/**
* The menu tree service.
*
* @var \Drupal\Core\Menu\MenuLinkTreeInterface
*/
protected $menuTree;
/**
* The link generator.
*
* @var \Drupal\Core\Utility\LinkGeneratorInterface
*/
protected $linkGenerator;
/**
* The overview tree form.
*
* @var array
*/
protected $overviewTreeForm = array(
'#tree' => TRUE,
);
/**
* Constructs a MenuForm object.
*
* @param \Drupal\Core\Entity\Query\QueryFactory $entity_query_factory
* The factory for entity queries.
* @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager
* The menu link manager.
* @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree
* The menu tree service.
* @param \Drupal\Core\Utility\LinkGeneratorInterface $link_generator
* The link generator.
*/
public function __construct(QueryFactory $entity_query_factory, MenuLinkManagerInterface $menu_link_manager, MenuLinkTreeInterface $menu_tree, LinkGeneratorInterface $link_generator) {
$this->entityQueryFactory = $entity_query_factory;
$this->menuLinkManager = $menu_link_manager;
$this->menuTree = $menu_tree;
$this->linkGenerator = $link_generator;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container
->get('entity.query'), $container
->get('plugin.manager.menu.link'), $container
->get('menu.link_tree'), $container
->get('link_generator'));
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$menu = $this->entity;
if ($this->operation == 'edit') {
$form['#title'] = $this
->t('Edit menu %label', array(
'%label' => $menu
->label(),
));
}
$form['label'] = array(
'#type' => 'textfield',
'#title' => $this
->t('Title'),
'#default_value' => $menu
->label(),
'#required' => TRUE,
);
$form['id'] = array(
'#type' => 'machine_name',
'#title' => $this
->t('Menu name'),
'#default_value' => $menu
->id(),
'#maxlength' => MENU_MAX_MENU_NAME_LENGTH_UI,
'#description' => $this
->t('A unique name to construct the URL for the menu. It must only contain lowercase letters, numbers and hyphens.'),
'#machine_name' => array(
'exists' => array(
$this,
'menuNameExists',
),
'source' => array(
'label',
),
'replace_pattern' => '[^a-z0-9-]+',
'replace' => '-',
),
// A menu's machine name cannot be changed.
'#disabled' => !$menu
->isNew() || $menu
->isLocked(),
);
$form['description'] = array(
'#type' => 'textfield',
'#title' => t('Administrative summary'),
'#maxlength' => 512,
'#default_value' => $menu
->getDescription(),
);
$form['langcode'] = array(
'#type' => 'language_select',
'#title' => t('Menu language'),
'#languages' => LanguageInterface::STATE_ALL,
'#default_value' => $menu
->language()
->getId(),
);
// Add menu links administration form for existing menus.
if (!$menu
->isNew() || $menu
->isLocked()) {
// Form API supports constructing and validating self-contained sections
// within forms, but does not allow handling the form section's submission
// equally separated yet. Therefore, we use a $form_state key to point to
// the parents of the form section.
// @see self::submitOverviewForm()
$form_state
->set('menu_overview_form_parents', [
'links',
]);
$form['links'] = array();
$form['links'] = $this
->buildOverviewForm($form['links'], $form_state);
}
return parent::form($form, $form_state);
}
/**
* Returns whether a menu name already exists.
*
* @param string $value
* The name of the menu.
*
* @return bool
* Returns TRUE if the menu already exists, FALSE otherwise.
*/
public function menuNameExists($value) {
// Check first to see if a menu with this ID exists.
if ($this->entityQueryFactory
->get('menu')
->condition('id', $value)
->range(0, 1)
->count()
->execute()) {
return TRUE;
}
// Check for a link assigned to this menu.
return $this->menuLinkManager
->menuNameInUse($value);
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$menu = $this->entity;
if (!$menu
->isNew() || $menu
->isLocked()) {
$this
->submitOverviewForm($form, $form_state);
}
$status = $menu
->save();
$edit_link = $this->entity
->link($this
->t('Edit'));
if ($status == SAVED_UPDATED) {
drupal_set_message($this
->t('Menu %label has been updated.', array(
'%label' => $menu
->label(),
)));
$this
->logger('menu')
->notice('Menu %label has been updated.', array(
'%label' => $menu
->label(),
'link' => $edit_link,
));
}
else {
drupal_set_message($this
->t('Menu %label has been added.', array(
'%label' => $menu
->label(),
)));
$this
->logger('menu')
->notice('Menu %label has been added.', array(
'%label' => $menu
->label(),
'link' => $edit_link,
));
}
$form_state
->setRedirectUrl($this->entity
->urlInfo('edit-form'));
}
/**
* Form constructor to edit an entire menu tree at once.
*
* Shows for one menu the menu links accessible to the current user and
* relevant operations.
*
* This form constructor can be integrated as a section into another form. It
* relies on the following keys in $form_state:
* - menu: A menu entity.
* - menu_overview_form_parents: An array containing the parent keys to this
* form.
* Forms integrating this section should call menu_overview_form_submit() from
* their form submit handler.
*/
protected function buildOverviewForm(array &$form, FormStateInterface $form_state) {
// Ensure that menu_overview_form_submit() knows the parents of this form
// section.
if (!$form_state
->has('menu_overview_form_parents')) {
$form_state
->set('menu_overview_form_parents', []);
}
$form['#attached']['library'][] = 'menu_ui/drupal.menu_ui.adminforms';
$tree = $this->menuTree
->load($this->entity
->id(), new MenuTreeParameters());
// We indicate that a menu administrator is running the menu access check.
$this
->getRequest()->attributes
->set('_menu_admin', TRUE);
$manipulators = array(
array(
'callable' => 'menu.default_tree_manipulators:checkAccess',
),
array(
'callable' => 'menu.default_tree_manipulators:generateIndexAndSort',
),
);
$tree = $this->menuTree
->transform($tree, $manipulators);
$this
->getRequest()->attributes
->set('_menu_admin', FALSE);
// Determine the delta; the number of weights to be made available.
$count = function (array $tree) {
$sum = function ($carry, MenuLinkTreeElement $item) {
return $carry + $item
->count();
};
return array_reduce($tree, $sum);
};
$delta = max($count($tree), 50);
$form['links'] = array(
'#type' => 'table',
'#theme' => 'table__menu_overview',
'#header' => array(
$this
->t('Menu link'),
array(
'data' => $this
->t('Enabled'),
'class' => array(
'checkbox',
),
),
$this
->t('Weight'),
array(
'data' => $this
->t('Operations'),
'colspan' => 3,
),
),
'#attributes' => array(
'id' => 'menu-overview',
),
'#tabledrag' => array(
array(
'action' => 'match',
'relationship' => 'parent',
'group' => 'menu-parent',
'subgroup' => 'menu-parent',
'source' => 'menu-id',
'hidden' => TRUE,
'limit' => \Drupal::menuTree()
->maxDepth() - 1,
),
array(
'action' => 'order',
'relationship' => 'sibling',
'group' => 'menu-weight',
),
),
);
$form['links']['#empty'] = $this
->t('There are no menu links yet. <a href=":url">Add link</a>.', [
':url' => $this
->url('entity.menu.add_link_form', [
'menu' => $this->entity
->id(),
], [
'query' => [
'destination' => $this->entity
->url('edit-form'),
],
]),
]);
$links = $this
->buildOverviewTreeForm($tree, $delta);
foreach (Element::children($links) as $id) {
if (isset($links[$id]['#item'])) {
$element = $links[$id];
$form['links'][$id]['#item'] = $element['#item'];
// TableDrag: Mark the table row as draggable.
$form['links'][$id]['#attributes'] = $element['#attributes'];
$form['links'][$id]['#attributes']['class'][] = 'draggable';
$form['links'][$id]['#item'] = $element['#item'];
// TableDrag: Sort the table row according to its existing/configured weight.
$form['links'][$id]['#weight'] = $element['#item']->link
->getWeight();
// Add special classes to be used for tabledrag.js.
$element['parent']['#attributes']['class'] = array(
'menu-parent',
);
$element['weight']['#attributes']['class'] = array(
'menu-weight',
);
$element['id']['#attributes']['class'] = array(
'menu-id',
);
$form['links'][$id]['title'] = array(
array(
'#theme' => 'indentation',
'#size' => $element['#item']->depth - 1,
),
$element['title'],
);
$form['links'][$id]['enabled'] = $element['enabled'];
$form['links'][$id]['enabled']['#wrapper_attributes']['class'] = array(
'checkbox',
'menu-enabled',
);
$form['links'][$id]['weight'] = $element['weight'];
// Operations (dropbutton) column.
$form['links'][$id]['operations'] = $element['operations'];
$form['links'][$id]['id'] = $element['id'];
$form['links'][$id]['parent'] = $element['parent'];
}
}
return $form;
}
/**
* Recursive helper function for buildOverviewForm().
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* The tree retrieved by \Drupal\Core\Menu\MenuLinkTreeInterface::load().
* @param int $delta
* The default number of menu items used in the menu weight selector is 50.
*
* @return array
* The overview tree form.
*/
protected function buildOverviewTreeForm($tree, $delta) {
$form =& $this->overviewTreeForm;
$tree_access_cacheability = new CacheableMetadata();
foreach ($tree as $element) {
$tree_access_cacheability = $tree_access_cacheability
->merge(CacheableMetadata::createFromObject($element->access));
// Only render accessible links.
if (!$element->access
->isAllowed()) {
continue;
}
/** @var \Drupal\Core\Menu\MenuLinkInterface $link */
$link = $element->link;
if ($link) {
$id = 'menu_plugin_id:' . $link
->getPluginId();
$form[$id]['#item'] = $element;
$form[$id]['#attributes'] = $link
->isEnabled() ? array(
'class' => array(
'menu-enabled',
),
) : array(
'class' => array(
'menu-disabled',
),
);
$form[$id]['title'] = Link::fromTextAndUrl($link
->getTitle(), $link
->getUrlObject())
->toRenderable();
if (!$link
->isEnabled()) {
$form[$id]['title']['#suffix'] = ' (' . $this
->t('disabled') . ')';
}
elseif ($id === 'menu_plugin_id:user.logout') {
$form[$id]['title']['#suffix'] = ' (' . $this
->t('<q>Log in</q> for anonymous users') . ')';
}
elseif (($url = $link
->getUrlObject()) && $url
->isRouted() && $url
->getRouteName() == 'user.page') {
$form[$id]['title']['#suffix'] = ' (' . $this
->t('logged in users only') . ')';
}
$form[$id]['enabled'] = array(
'#type' => 'checkbox',
'#title' => $this
->t('Enable @title menu link', array(
'@title' => $link
->getTitle(),
)),
'#title_display' => 'invisible',
'#default_value' => $link
->isEnabled(),
);
$form[$id]['weight'] = array(
'#type' => 'weight',
'#delta' => $delta,
'#default_value' => $link
->getWeight(),
'#title' => $this
->t('Weight for @title', array(
'@title' => $link
->getTitle(),
)),
'#title_display' => 'invisible',
);
$form[$id]['id'] = array(
'#type' => 'hidden',
'#value' => $link
->getPluginId(),
);
$form[$id]['parent'] = array(
'#type' => 'hidden',
'#default_value' => $link
->getParent(),
);
// Build a list of operations.
$operations = array();
$operations['edit'] = array(
'title' => $this
->t('Edit'),
);
// Allow for a custom edit link per plugin.
$edit_route = $link
->getEditRoute();
if ($edit_route) {
$operations['edit']['url'] = $edit_route;
// Bring the user back to the menu overview.
$operations['edit']['query'] = $this
->getDestinationArray();
}
else {
// Fall back to the standard edit link.
$operations['edit'] += array(
'url' => Url::fromRoute('menu_ui.link_edit', [
'menu_link_plugin' => $link
->getPluginId(),
]),
);
}
// Links can either be reset or deleted, not both.
if ($link
->isResettable()) {
$operations['reset'] = array(
'title' => $this
->t('Reset'),
'url' => Url::fromRoute('menu_ui.link_reset', [
'menu_link_plugin' => $link
->getPluginId(),
]),
);
}
elseif ($delete_link = $link
->getDeleteRoute()) {
$operations['delete']['url'] = $delete_link;
$operations['delete']['query'] = $this
->getDestinationArray();
$operations['delete']['title'] = $this
->t('Delete');
}
if ($link
->isTranslatable()) {
$operations['translate'] = array(
'title' => $this
->t('Translate'),
'url' => $link
->getTranslateRoute(),
);
}
$form[$id]['operations'] = array(
'#type' => 'operations',
'#links' => $operations,
);
}
if ($element->subtree) {
$this
->buildOverviewTreeForm($element->subtree, $delta);
}
}
$tree_access_cacheability
->merge(CacheableMetadata::createFromRenderArray($form))
->applyTo($form);
return $form;
}
/**
* Submit handler for the menu overview form.
*
* This function takes great care in saving parent items first, then items
* underneath them. Saving items in the incorrect order can break the tree.
*/
protected function submitOverviewForm(array $complete_form, FormStateInterface $form_state) {
// Form API supports constructing and validating self-contained sections
// within forms, but does not allow to handle the form section's submission
// equally separated yet. Therefore, we use a $form_state key to point to
// the parents of the form section.
$parents = $form_state
->get('menu_overview_form_parents');
$input = NestedArray::getValue($form_state
->getUserInput(), $parents);
$form =& NestedArray::getValue($complete_form, $parents);
// When dealing with saving menu items, the order in which these items are
// saved is critical. If a changed child item is saved before its parent,
// the child item could be saved with an invalid path past its immediate
// parent. To prevent this, save items in the form in the same order they
// are sent, ensuring parents are saved first, then their children.
// See https://www.drupal.org/node/181126#comment-632270.
$order = is_array($input) ? array_flip(array_keys($input)) : array();
// Update our original form with the new order.
$form = array_intersect_key(array_merge($order, $form), $form);
$fields = array(
'weight',
'parent',
'enabled',
);
$form_links = $form['links'];
foreach (Element::children($form_links) as $id) {
if (isset($form_links[$id]['#item'])) {
$element = $form_links[$id];
$updated_values = array();
// Update any fields that have changed in this menu item.
foreach ($fields as $field) {
if ($element[$field]['#value'] != $element[$field]['#default_value']) {
$updated_values[$field] = $element[$field]['#value'];
}
}
if ($updated_values) {
// Use the ID from the actual plugin instance since the hidden value
// in the form could be tampered with.
$this->menuLinkManager
->updateDefinition($element['#item']->link
->getPLuginId(), $updated_values);
}
}
}
}
}