Create a dynamic menu link

What's a dynamic menu link? Check out the "My account" link in the account menu when you're logged in. The url is /user, but when you click on it, it redirects to user/1 (or whatever your user ID is). How does this work?

Here is the route config for the path, found in user.routing.yml

user.page:
  path: '/user'
  defaults:
    _controller: '\Drupal\user\Controller\UserController::userPage'
    _title: 'My account'
  requirements:
    _user_is_logged_in: 'TRUE'

When someone goes to /user, it will call UserController::userPage to determine what to serve.

  /**
   * Redirects users to their profile page.
   *
   * This controller assumes that it is only invoked for authenticated users.
   * This is enforced for the 'user.page' route with the '_user_is_logged_in'
   * requirement.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse
   *   Returns a redirect to the profile of the currently logged in user.
   */
  public function userPage() {
    return $this->redirect('entity.user.canonical', ['user' => $this->currentUser()->id()]);
  }

In this function, it redirects to entity.user.canonical with the user argument set to the current user's ID.

 

Great, so what can you do with this?

I needed to provide a link to a Commerce Store from the user menu for the owner of the store. Here's how I set that up:

custom_module.my_store
  path: '/my_store'
  defaults:
    _controller: '\Drupal\custum_module\Controller\CustomModuleMyStore::myStore'
    _title: 'My store'
  requirements:
    _role: 'store_owner'
  public function myStore() {
    $current_user = \Drupal::currentUser();

    $owner_stores = \Drupal::entityQuery('commerce_store')
      ->condition('uid', $current_user->id())
      ->execute();

    if ($owner_stores) {
      return $this->redirect('entity.commerce_store.canonical', ['commerce_store' => array_pop($owner_stores)]);
    }
  }

The routing config is similar, we can change the path, title, and any access requirements. See more info on that here: https://www.drupal.org/node/2092643

The big difference is that we're pointing to a custom controller, CustomModuleMyStore. This has the function myStore that will look up the store for the user. It gets the current user and does an entityquery to find stores they own, the redirects to the first store returned. 

One thing that confused me here was I couldn't find a route for entity.commerce_store.canonical by searching the code, but I did see links defined for it in commerce_store.links.task.yml so I knew it existed. I found it in the class annotations for the Store entity:

 *   links = {
 *     "canonical" = "/store/{commerce_store}",
 *     "add-page" = "/store/add",
 *     "add-form" = "/store/add/{commerce_store_type}",
 *     "edit-form" = "/store/{commerce_store}/edit",
 *     "delete-form" = "/store/{commerce_store}/delete",
 *     "delete-multiple-form" = "/admin/commerce/config/stores/delete",
 *     "collection" = "/admin/commerce/config/stores",
 *   },

This is just a snippet of the annotation, check out the full thing in the Store class (or just look at Node if you don't have commerce installed). The links here get turned into routes like entity.commerce_store.canonical and entity.commerce_store.edit_form. One of the many Drupalisms to keep in mind!

 

There's more to this, like setting up a menu link and refining permissions handling. All that to be explored later!

Tags
Routing