MenuForm.php 14.9 KB
Newer Older
1 2 3 4
<?php

/**
 * @file
5
 * Contains \Drupal\menu_ui\MenuForm.
6 7
 */

8
namespace Drupal\menu_ui;
9

10
use Drupal\Component\Utility\NestedArray;
11
use Drupal\Core\Entity\EntityForm;
12
use Drupal\Core\Entity\Query\QueryFactory;
13
use Drupal\Core\Language\Language;
14
use Drupal\Core\Render\Element;
15
use Drupal\menu_link\MenuLinkStorageInterface;
16
use Drupal\menu_link\MenuTreeInterface;
17
use Symfony\Component\DependencyInjection\ContainerInterface;
18 19

/**
20
 * Base form for menu edit forms.
21
 */
22
class MenuForm extends EntityForm {
23

24 25 26 27 28 29 30 31
  /**
   * The factory for entity queries.
   *
   * @var \Drupal\Core\Entity\Query\QueryFactory
   */
  protected $entityQueryFactory;

  /**
32
   * The menu link storage.
33
   *
34
   * @var \Drupal\menu_link\MenuLinkStorageInterface
35 36 37
   */
  protected $menuLinkStorage;

38 39 40 41 42 43 44
  /**
   * The menu tree service.
   *
   * @var \Drupal\menu_link\MenuTreeInterface
   */
  protected $menuTree;

45 46 47 48 49 50 51 52
  /**
   * The overview tree form.
   *
   * @var array
   */
  protected $overviewTreeForm = array('#tree' => TRUE);

  /**
53
   * Constructs a MenuForm object.
54 55 56
   *
   * @param \Drupal\Core\Entity\Query\QueryFactory $entity_query_factory
   *   The factory for entity queries.
57 58
   * @param \Drupal\menu_link\MenuLinkStorageInterface $menu_link_storage
   *   The menu link storage.
59 60
   * @param \Drupal\menu_link\MenuTreeInterface $menu_tree
   *   The menu tree service.
61
   */
62
  public function __construct(QueryFactory $entity_query_factory, MenuLinkStorageInterface $menu_link_storage, MenuTreeInterface $menu_tree) {
63 64
    $this->entityQueryFactory = $entity_query_factory;
    $this->menuLinkStorage = $menu_link_storage;
65
    $this->menuTree = $menu_tree;
66 67 68 69 70
  }

  /**
   * {@inheritdoc}
   */
71
  public static function create(ContainerInterface $container) {
72 73
    return new static(
      $container->get('entity.query'),
74
      $container->get('entity.manager')->getStorage('menu_link'),
75
      $container->get('menu_link.tree')
76 77 78
    );
  }

79
  /**
80
   * {@inheritdoc}
81
   */
82 83
  public function form(array $form, array &$form_state) {
    $menu = $this->entity;
84 85

    if ($this->operation == 'edit') {
86
      $form['#title'] = $this->t('Edit menu %label', array('%label' => $menu->label()));
87 88
    }

89 90 91 92 93 94 95 96 97 98 99 100 101
    $form['label'] = array(
      '#type' => 'textfield',
      '#title' => t('Title'),
      '#default_value' => $menu->label(),
      '#required' => TRUE,
    );
    $form['id'] = array(
      '#type' => 'machine_name',
      '#title' => t('Menu name'),
      '#default_value' => $menu->id(),
      '#maxlength' => MENU_MAX_MENU_NAME_LENGTH_UI,
      '#description' => t('A unique name to construct the URL for the menu. It must only contain lowercase letters, numbers and hyphens.'),
      '#machine_name' => array(
102
        'exists' => array($this, 'menuNameExists'),
103 104 105 106 107
        'source' => array('label'),
        'replace_pattern' => '[^a-z0-9-]+',
        'replace' => '-',
      ),
      // A menu's machine name cannot be changed.
108
      '#disabled' => !$menu->isNew() || $menu->isLocked(),
109 110
    );
    $form['description'] = array(
111 112 113
      '#type' => 'textfield',
      '#title' => t('Administrative summary'),
      '#maxlength' => 512,
114 115
      '#default_value' => $menu->description,
    );
116

117 118 119 120 121 122 123 124 125 126 127 128
    $form['langcode'] = array(
      '#type' => 'language_select',
      '#title' => t('Menu language'),
      '#languages' => Language::STATE_ALL,
      '#default_value' => $menu->langcode,
    );
    // Unlike the menu langcode, the default language configuration for menu
    // links only works with language module installed.
    if ($this->moduleHandler->moduleExists('language')) {
      $form['default_menu_links_language'] = array(
        '#type' => 'details',
        '#title' => t('Menu links language'),
129
        '#open' => TRUE,
130 131 132 133 134 135 136 137 138 139 140
      );
      $form['default_menu_links_language']['default_language'] = array(
        '#type' => 'language_configuration',
        '#entity_information' => array(
          'entity_type' => 'menu_link',
          'bundle' => $menu->id(),
        ),
        '#default_value' => language_get_default_configuration('menu_link', $menu->id()),
      );
    }

141
    // Add menu links administration form for existing menus.
142
    if (!$menu->isNew() || $menu->isLocked()) {
143 144 145 146
      // 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.
147
      // @see self::submitOverviewForm()
148 149
      $form_state['menu_overview_form_parents'] = array('links');
      $form['links'] = array();
150
      $form['links'] = $this->buildOverviewForm($form['links'], $form_state);
151 152
    }

153
    return parent::form($form, $form_state);
154 155
  }

156
  /**
157 158 159 160 161 162 163 164 165 166 167 168 169 170
   * 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;
    }

171 172
    // Check for a link assigned to this menu.
    return $this->entityQueryFactory->get('menu_link')->condition('menu_name', $value)->range(0, 1)->count()->execute();
173 174 175 176
  }

  /**
   * {@inheritdoc}
177 178 179 180
   */
  protected function actions(array $form, array &$form_state) {
    $actions = parent::actions($form, $form_state);

181 182 183 184 185 186 187 188 189
    // Add the language configuration submit handler. This is needed because the
    // submit button has custom submit handlers.
    if ($this->moduleHandler->moduleExists('language')) {
      array_unshift($actions['submit']['#submit'],'language_configuration_element_submit');
      array_unshift($actions['submit']['#submit'], array($this, 'languageConfigurationSubmit'));
    }
    // We cannot leverage the regular submit handler definition because we have
    // button-specific ones here. Hence we need to explicitly set it for the
    // submit action, otherwise it would be ignored.
190 191
    if ($this->moduleHandler->moduleExists('content_translation')) {
      array_unshift($actions['submit']['#submit'], 'content_translation_language_configuration_element_submit');
192
    }
193 194 195
    return $actions;
  }

196 197 198 199 200 201 202 203 204 205 206 207 208
  /**
   * Submit handler to update the bundle for the default language configuration.
   */
  public function languageConfigurationSubmit(array &$form, array &$form_state) {
    // Since the machine name is not known yet, and it can be changed anytime,
    // we have to also update the bundle property for the default language
    // configuration in order to have the correct bundle value.
    $form_state['language']['default_language']['bundle'] = $form_state['values']['id'];
    // Clear cache so new menus (bundles) show on the language settings admin
    // page.
    entity_info_cache_clear();
  }

209
  /**
210
   * {@inheritdoc}
211 212
   */
  public function save(array $form, array &$form_state) {
213
    $menu = $this->entity;
214
    if (!$menu->isNew() || $menu->isLocked()) {
215
      $this->submitOverviewForm($form, $form_state);
216
    }
217 218 219

    $status = $menu->save();

220
    $edit_link = \Drupal::linkGenerator()->generateFromUrl($this->t('Edit'), $this->entity->urlInfo());
221 222
    if ($status == SAVED_UPDATED) {
      drupal_set_message(t('Menu %label has been updated.', array('%label' => $menu->label())));
223
      watchdog('menu', 'Menu %label has been updated.', array('%label' => $menu->label()), WATCHDOG_NOTICE, $edit_link);
224 225 226
    }
    else {
      drupal_set_message(t('Menu %label has been added.', array('%label' => $menu->label())));
227
      watchdog('menu', 'Menu %label has been added.', array('%label' => $menu->label()), WATCHDOG_NOTICE, $edit_link);
228 229
    }

230
    $form_state['redirect_route'] = $this->entity->urlInfo('edit-form');
231 232 233 234 235 236 237 238 239 240
  }

  /**
   * 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:
241
   * - menu: A loaded menu definition, as returned by menu_ui_load().
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
   * - 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, array &$form_state) {
    // Ensure that menu_overview_form_submit() knows the parents of this form
    // section.
    $form['#tree'] = TRUE;
    $form['#theme'] = 'menu_overview_form';
    $form_state += array('menu_overview_form_parents' => array());

    $form['#attached']['css'] = array(drupal_get_path('module', 'menu') . '/css/menu.admin.css');

    $links = array();
    $query = $this->entityQueryFactory->get('menu_link')
      ->condition('menu_name', $this->entity->id());
    for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) {
      $query->sort('p' . $i, 'ASC');
    }
    $result = $query->execute();

    if (!empty($result)) {
      $links = $this->menuLinkStorage->loadMultiple($result);
    }

    $delta = max(count($links), 50);
    // We indicate that a menu administrator is running the menu access check.
270
    $this->getRequest()->attributes->set('_menu_admin', TRUE);
271
    $tree = $this->menuTree->buildTreeData($links);
272
    $this->getRequest()->attributes->set('_menu_admin', FALSE);
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303

    $form = array_merge($form, $this->buildOverviewTreeForm($tree, $delta));
    $form['#empty_text'] = t('There are no menu links yet. <a href="@link">Add link</a>.', array('@link' => url('admin/structure/menu/manage/' . $this->entity->id() .'/add')));

    return $form;
  }

  /**
   * Recursive helper function for buildOverviewForm().
   *
   * @param $tree
   *   The menu_tree retrieved by menu_tree_data.
   * @param $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;
    foreach ($tree as $data) {
      $item = $data['link'];
      // Don't show callbacks; these have $item['hidden'] < 0.
      if ($item && $item['hidden'] >= 0) {
        $mlid = 'mlid:' . $item['mlid'];
        $form[$mlid]['#item'] = $item;
        $form[$mlid]['#attributes'] = $item['hidden'] ? array('class' => array('menu-disabled')) : array('class' => array('menu-enabled'));
        $form[$mlid]['title']['#markup'] = l($item['title'], $item['href'], $item['localized_options']);
        if ($item['hidden']) {
          $form[$mlid]['title']['#markup'] .= ' (' . t('disabled') . ')';
        }
304
        elseif ($item['link_path'] == 'user' && $item['module'] == 'user') {
305 306 307 308 309 310 311 312 313 314 315 316 317 318
          $form[$mlid]['title']['#markup'] .= ' (' . t('logged in users only') . ')';
        }

        $form[$mlid]['hidden'] = array(
          '#type' => 'checkbox',
          '#title' => t('Enable @title menu link', array('@title' => $item['title'])),
          '#title_display' => 'invisible',
          '#default_value' => !$item['hidden'],
        );
        $form[$mlid]['weight'] = array(
          '#type' => 'weight',
          '#delta' => $delta,
          '#default_value' => $item['weight'],
          '#title' => t('Weight for @title', array('@title' => $item['title'])),
319
          '#title_display' => 'invisible',
320 321 322 323 324 325 326 327 328 329 330
        );
        $form[$mlid]['mlid'] = array(
          '#type' => 'hidden',
          '#value' => $item['mlid'],
        );
        $form[$mlid]['plid'] = array(
          '#type' => 'hidden',
          '#default_value' => $item['plid'],
        );
        // Build a list of operations.
        $operations = array();
331
        $operations['edit'] = array(
332 333 334
          'title' => t('Edit'),
          'href' => 'admin/structure/menu/item/' . $item['mlid'] . '/edit',
        );
335
        // Only items created by the Menu UI module can be deleted.
336 337
        if ($item->access('delete')) {
          $operations['delete'] = array(
338 339 340 341 342
            'title' => t('Delete'),
            'href' => 'admin/structure/menu/item/' . $item['mlid'] . '/delete',
          );
        }
        // Set the reset column.
343 344
        elseif ($item->access('reset')) {
          $operations['reset'] = array(
345 346 347 348 349 350
            'title' => t('Reset'),
            'href' => 'admin/structure/menu/item/' . $item['mlid'] . '/reset',
          );
        }
        $form[$mlid]['operations'] = array(
          '#type' => 'operations',
351
          '#links' => $operations,
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
        );
      }

      if ($data['below']) {
        $this->buildOverviewTreeForm($data['below'], $delta);
      }
    }
    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 menu tree.
   */
  protected function submitOverviewForm(array $complete_form, array &$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['menu_overview_form_parents'];
    $input = NestedArray::getValue($form_state['input'], $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 http://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);

    $updated_items = array();
    $fields = array('weight', 'plid');
389
    foreach (Element::children($form) as $mlid) {
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412
      if (isset($form[$mlid]['#item'])) {
        $element = $form[$mlid];
        // Update any fields that have changed in this menu item.
        foreach ($fields as $field) {
          if ($element[$field]['#value'] != $element[$field]['#default_value']) {
            $element['#item'][$field] = $element[$field]['#value'];
            $updated_items[$mlid] = $element['#item'];
          }
        }
        // Hidden is a special case, the value needs to be reversed.
        if ($element['hidden']['#value'] != $element['hidden']['#default_value']) {
          // Convert to integer rather than boolean due to PDO cast to string.
          $element['#item']['hidden'] = $element['hidden']['#value'] ? 0 : 1;
          $updated_items[$mlid] = $element['#item'];
        }
      }
    }

    // Save all our changed items to the database.
    foreach ($updated_items as $item) {
      $item['customized'] = 1;
      $item->save();
    }
413 414 415
  }

}