I learn best when I have a problem to solve, and with one of our D8 upgrade projects, we had a mess to clean up in the menu system. This provided an excellent oppportunity to get hands-on in Drupal core, learning some of the major differences from earlier versions, and three things in particular:

  1. How easy it is to use Drupal Console to create your own tools
  2. How much the menu system has changed
  3. How to load and save just about any entity

The problem

It seemed like an easy one: links on the menu were going to internal "node" paths instead of the aliased "friendly URL". For example, a link on a submenu went to "/node/494" instead of "/about/accreditation". In this particular project, there were several hundred links that got mangled in this way.

I'm not exactly sure how this happened, but it has something to do with starting the migration in Drupal 8.0, updating the migration to work with 8.1, and going back to clean up all the friendly URLs. Somewhere along the way, the menu system ended up with links to the node/[nid] path, and not the friendly URLs, even though the pages themselves worked fine with the URL aliases.

The Redirect module was already set up, and configured to redirect to the alias -- but this does not seem to work correctly yet.

Bulk operations were of no help -- and worse, the menu and parent items on the menu link edit form on most of the links did not show up correctly, even though the menu itself showed links in the correct place.

Creating a repair tool using Drupal Console

In earlier versions of Drupal, we've created dozens of little helper scripts using Drush. It's relatively easy to create a drush script to fix up data, set up integrations with other systems, diagnose issues, etc. Drush was our go-to tool for creating little helper tools.

In Drupal 8, Drupal Console has become a compelling alternative -- mainly due to its built-in code generation. Drush is still an important part of our kit, but Drupal Console has quickly become an important complement.

And it turns out, one of the things you can generate in Drupal Console is a Drupal Console command. First, create a module (if you don't want to add the command to an existing module):

john@katmai >git/test (develop)> drupal generate:module

 Enter the new module name:
 > Fix Menus

 Enter the module machine name [fix_menus]:
 >

 Enter the module Path [/modules/custom]:
 >

 Enter module description [My Awesome Module]:
 > Fix non-aliased menu link data

 Enter package name [Custom]:
 >

 Enter Drupal Core version [8.x]:
 >

 Do you want to generate a .module file (yes/no) [yes]:
 > no

 Define module as feature (yes/no) [no]:
 >

 Do you want to add a composer.json file to your module (yes/no) [yes]:
 > no

 Would you like to add module dependencies (yes/no) [no]:
 > no


 Do you confirm generation? (yes/no) [yes]:
 >

Generated or updated files
 Site path: /home/john/git/test
 1 - modules/custom/fix_menus/fix_menus.info.yml
john@katmai >git/test (develop)>

Next, generate your custom command:

john@katmai >git/test (develop)> drupal generate:command
 Enter the module name [ctools]:
 > fix_menus

 Enter the Command name. [fix_menus:default]:
 > fix_aliases

 Enter the Command Class. (Must end with the word 'Command'). [DefaultCommand]:
 > FixAliasesCommand

 Is the command aware of the drupal site installation when executed?. (yes/no) [yes]:
 >


 Do you confirm generation? (yes/no) [yes]:
 >

Generated or updated files
 Site path: /home/john/git/test
 1 - modules/custom/fix_menus/src/Command/FixAliasesCommand.php
john@katmai >git/test (develop)>

... And just like that, you have a module with a Drupal Console command that's all hooked up and ready for code, runnable as soon as you enable the new module.

In the new module, you primarily need to fill in the execute method in the generated file.

One thing I have not yet figured out is how to get PhpStorm to be intelligent about Drupal Console classes, like Drupal\Console\Style\DrupalStyle. But it's not hard to find info about the Symfony InputInterface and OutputInterfaces.

Menu System Changes

I've heard that the menu system is one of those areas that many Drupal folk think needs major improvement. While I'm not sure how improved it is in D8, it has certainly changed -- a lot.

In Drupal 7, menu links were basically rows in the menu_links table. Other than external links (which begin with "http://"), links in D7 and D6 pointed to specific items in the system -- views paths, nodes, taxonomy terms, etc. While you could enter an alias in a menu link, it got converted to the system path on save. And, as a result, a menu link pointing to a node always pointed to node/[nid] in the underlying table. When the menu was rendered, the system automatically replaced all system paths with the corresponding alias (friendly URL) if one exists.

This system is quite familiar to anyone with Drupal experience. Menu links pointing to unpublished nodes are completely invisible, always. Links to content you don't have access to are always hidden as well.

The Drupal 8 Menu System

Drupal 8 changes this entirely. You can still have links to entities, views, panels, terms, users, etc. just like D6 and D7 -- however, you can also have links to arbitrary paths inside the site, disconnected from whatever actually gets loaded at that path. These links may each be enabled or disabled, but they are otherwise disconnected from whatever loads at this URL.

Menu links are now content entities, like many other entities in the system, with an entity type of "MenuLinkContent." While there is no UI for attaching fields currently in Drupal 8 core, as a content entity you interact with it much the same as with other content entities, by loading and saving it.

So if you add a link using a regular entity edit form (e.g. by editing a node or a view), the menu link gets created with a direct reference to that content, and rewritten to whatever alias it has whenever the cache is rebuilt.

However, when you migrate in menu links, it uses the new "base" style.

In the menu_link_content_data table in the database, you will see most migrated links listed with a link__uri value beginning with "internal:/" followed by the migrated path. However, if you add a menu link to a node form, it saves as "entity:node/[nid]" instead.

On the MenuLinkContent entity, the "internal" links are considered Unrouted, while the entity links are Routed. These two varieties have quite different structure under the hood -- the question I had was how to change from one to another. Nobody on IRC seemed to have an answer, and the documentation seems pretty lacking in this area -- I found issues discussing the new structure, but no documentation for changing from one variety to the other.

So I rolled up my sleeves and debugged the menu link edit form, to see how this problem got solved.

And it turns out, it's pretty simple: just set the link item's link->uri to either 'base:node/[nid]' to get an unrouted link or to 'entity:node/[nid]' to get a routed one, and save it.

This still lost the menu's place in the overall hierarchy, but I was able to fix that by walking the menu tree, and setting the menu name and parent link on each child link.

Loading and Saving Entities

I've already spent quite some time in the D8 migration system, but this was really my first foray into the heart of Drupal 8's API, for far more usual things like updating an entity. How you do this seems to be in flux -- the approach I found to work, using \Drupal::entityManager(), is already marked deprecated.

When you walk the menu tree, you get link objects, but these are not actually entities and so you can't edit and save them directly. The link object does contain a UUID that can be used to load the actual entity, so you can update and save it.

So... here's the final code:

/**
 * {@inheritdoc}
 */
protected function execute(InputInterface $input, OutputInterface $output) {
  $io = new DrupalStyle($input, $output);

  $menu_tree_parameters = new MenuTreeParameters();
  $tree = \Drupal::menuTree()->load('main', $menu_tree_parameters);

  foreach($tree as $id => $menu){
    $this->walk_link($menu, $io);
  }
}

/**
 * @param $menu - Menu link to update, and load children
 * @param $io - Drupal Console io object, to print results
 * @param $parent - The parent menu link to set on this link
 *
 * Recursive function to update all links on a menu
 */
private function walk_link($menu, $io, $parent) {
  /** @var \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent $link */
  $link = $menu->link;

  if ($link->getProvider() == 'menu_link_content') {
    /** @var \Drupal\Core\Url $url */
    $url = $link->getUrlObject();
    if ($url->isRouted()){
      // This is already a routed link, ignore
    } else {
      $path = $url->getUri();

      if (preg_match('/^base:node\/(\d+)$/', $path, $match)) {
        // We have a node path... ding ding ding!
        $nid = $match[1];
        // Plugin ID looks like: menu_link_content:29efb9ab-cde8-4f3b-8878-9065a5af79b1
        list($type,$uuid) = explode(':', $link->getPluginId());
        // entityManager has been split into many different services -- getStorage
        // is now on entityTypeManager.
        $storage = \Drupal::entityTypeManager()->getStorage('menu_link_content');
        $entities = $storage->loadByProperties(['uuid' => $uuid]);
        /** @var \Drupal\menu_link_content\Entity\MenuLinkContent $entity */
        $entity = array_shift($entities);
        // Now update the entity:
        $entity->parent = $parent;
        $entity->menu_name = 'main';
        $entity->link->uri = 'entity:node/'.$nid;
        $entity->save();
        // Now print the change:
        $io->block($link->getTitle() . '('.$nid.'): ' . $path . ' -> entity:node/'.$nid);

      }
    }
  }

  // Now update the child links
  if ($menu->subtree) {
    $parent = $link->getPluginId();
    foreach ($menu->subtree as $submenu) {
      $this->walk_link($submenu, $io, $parent);
    }
  }
}

The Takeaways

Going through this process, I learned a lot about the internals of Drupal 8:

  • Drupal Console is a great tool to quickly get started on any new functionality you might need, and can be used to extend itself, even easier than drush.
  • The Drupal 8 menu system is more sophisticated than earlier versions, with distinctly different Routed and Unrouted links -- this is an area you need to understand to build sites more effectively.
  • The current Drupal to Drupal migration creates unrouted links (probably so they can import them before the entities are imported) -- but older versions of Drupal are more comparable to routed links.
  • Routed links appear in the "menu settings" of content editing forms. Unrouted links do not.
  • When you update a URL alias, the system does attempt to update unrouted menu links -- however, sometimes this results in rewriting to the internal unaliased link, and not the correct alias. Routed links (presumably) always update correctly.
  • The current (as of 8.1.x) "correct" way to load an individual entity, of any kind, is using entityTypeManager->getStorage(). You can call it directly using \Drupal::entityTypeManager(), or better, use dependency injection to load it as a service.
  • Drupal entities provide a bunch of "magic" getters/setters that allow you to set properties directly on the entity, in relatively simple ways. Unlike earlier Drupal versions, you don't actually need to drill down into the nested arrays of language, delta, value, etc -- you can often just set a property directly and the magic setter will set the appropriate value.
  • As usual, finding related code examples is often the best way to get familiar with how to interact with a particular internal API. An IDE like PhpStorm can be immensely helpful for both searching for related code and debugging your own code.

Any tips you'd like to add to this, or share? Please leave a comment below!

Didn't knew the generate:command is AWESOME!!!
Also, didn't knew about the Routed and unRouted links, you proably save me a huge future head pain with that!

Add new comment

The content of this field is kept private and will not be shown publicly.

Filtered HTML

  • Web page addresses and email addresses turn into links automatically.
  • Allowed HTML tags: <a href hreflang> <em> <strong> <blockquote cite> <cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h1> <h2 id> <h3 id> <h4 id> <h5 id> <p> <br> <img src alt height width>
  • Lines and paragraphs break automatically.
CAPTCHA
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.