diff --git a/core/core.services.yml b/core/core.services.yml
index 0bb13d0..fe354e1 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -63,6 +63,13 @@ services:
     factory_method: get
     factory_service: cache_factory
     arguments: [entity]
+  cache.menu:
+    class: Drupal\Core\Cache\CacheBackendInterface
+    tags:
+      - { name: cache.bin }
+    factory_method: get
+    factory_service: cache_factory
+    arguments: [menu]
   cache.render:
     class: Drupal\Core\Cache\CacheBackendInterface
     tags:
@@ -265,6 +272,21 @@ services:
   plugin.manager.action:
     class: Drupal\Core\Action\ActionManager
     arguments: ['@container.namespaces', '@cache.discovery', '@module_handler']
+  plugin.manager.menu.link:
+    class: Drupal\Core\Menu\MenuLinkManager
+    arguments: ['@menu.tree_storage', '@menu_link.static.overrides', '@module_handler']
+  menu.link_tree:
+    class: Drupal\Core\Menu\MenuLinkTree
+    arguments: ['@menu.tree_storage', '@plugin.manager.menu.link', '@router.route_provider', '@menu.active_trail', '@controller_resolver']
+  menu.default_tree_manipulators:
+    class: Drupal\Core\Menu\DefaultMenuLinkTreeManipulators
+    arguments: ['@access_manager', '@current_user']
+  menu.active_trail:
+    class: Drupal\Core\Menu\MenuActiveTrail
+    arguments: ['@plugin.manager.menu.link', '@current_route_match']
+  menu.parent_form_selector:
+    class: Drupal\Core\Menu\MenuParentFormSelector
+    arguments: ['@menu.link_tree', '@entity.manager']
   plugin.manager.menu.local_action:
     class: Drupal\Core\Menu\LocalActionManager
     arguments: ['@controller_resolver', '@request_stack', '@router.route_provider', '@module_handler', '@cache.discovery', '@language_manager', '@access_manager', '@current_user']
@@ -279,6 +301,13 @@ services:
     parent: default_plugin_manager
   plugin.cache_clearer:
     class: Drupal\Core\Plugin\CachedDiscoveryClearer
+  menu.tree_storage:
+    class: Drupal\Core\Menu\MenuTreeStorage
+    arguments: ['@database', '@cache.menu', 'menu_tree']
+    public: false  # Private to plugin.manager.menu.link and menu.link_tree
+  menu_link.static.overrides:
+    class: Drupal\Core\Menu\StaticMenuLinkOverrides
+    arguments: ['@config.factory']
   request:
     class: Symfony\Component\HttpFoundation\Request
     synthetic: true
diff --git a/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php
new file mode 100644
index 0000000..2238a75
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php
@@ -0,0 +1,175 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Core\Access\AccessManager;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Provides default menu link tree manipulators.
+ */
+class DefaultMenuLinkTreeManipulators {
+
+  /**
+   * The access manager.
+   *
+   * @var \Drupal\Core\Access\AccessManager
+   */
+  protected $accessManager;
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $account;
+
+  /**
+   * Constructs a \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators object.
+   *
+   * @param \Drupal\Core\Access\AccessManager $access_manager
+   *   The access manager.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The current user.
+   */
+  public function __construct(AccessManager $access_manager, AccountInterface $account) {
+    $this->accessManager = $access_manager;
+    $this->account = $account;
+  }
+
+  /**
+   * Menu link tree manipulator that performs access checks.
+   *
+   * Removes menu links from the given menu tree whose links are inaccessible
+   * for the current user, sets the 'access' property to TRUE on tree elements
+   * that are accessible for the current user.
+   *
+   * Makes the resulting menu tree impossible to render cache, unless render
+   * caching per user is acceptable.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
+   *   The menu link tree to manipulate.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
+   *   The manipulated menu link tree.
+   */
+  public function checkAccess(array $tree) {
+    foreach ($tree as $key => $element) {
+      // Other menu tree manipulators may already have calculated access, do
+      // not overwrite the existing value in that case.
+      if (!isset($element->access)) {
+        $tree[$key]->access = $this->menuLinkCheckAccess($element->link);
+      }
+      if ($tree[$key]->access) {
+        if ($tree[$key]->subtree) {
+          $tree[$key]->subtree = $this->checkAccess($tree[$key]->subtree);
+        }
+      }
+      else {
+        unset($tree[$key]);
+      }
+    }
+    return $tree;
+  }
+
+  /**
+   * Check access for one menu link instance.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkInterface $instance
+   *   The menu link instance.
+   *
+   * @return bool
+   *   TRUE if the current user can access the link, FALSE otherwise.
+   */
+  protected function menuLinkCheckAccess(MenuLinkInterface $instance) {
+    // Use the definition here since that's a lot faster than creating a Url
+    // object that we don't need.
+    $definition = $instance->getPluginDefinition();
+    // 'url' should only be populated for external links.
+    if (!empty($definition['url']) && empty($definition['route_name'])) {
+      $access = TRUE;
+    }
+    else {
+      $access = $this->accessManager->checkNamedRoute($definition['route_name'], $definition['route_parameters'], $this->account);
+    }
+    return $access;
+  }
+
+  /**
+   * Menu link tree manipulator that generates a unique index, and sorts by it.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
+   *   The menu link tree to manipulate.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
+   *   The manipulated menu link tree.
+   */
+  public function generateIndexAndSort(array $tree) {
+    $new_tree = array();
+    foreach ($tree as $key => $v) {
+      if ($tree[$key]->subtree) {
+        $tree[$key]->subtree = $this->generateIndexAndSort($tree[$key]->subtree);
+      }
+      $instance = $tree[$key]->link;
+      // The weights are made a uniform 5 digits by adding 50000 as an offset.
+      // After $this->menuLinkCheckAccess(), $instance->getTitle() has the
+      // localized or translated title. Adding the plugin id to the end of the
+      // index insures that it is unique.    }
+      $new_tree[(50000 + $instance->getWeight()) . ' ' . $instance->getTitle() . ' ' . $instance->getPluginId()] = $tree[$key];
+    }
+    ksort($new_tree);
+    return $new_tree;
+  }
+
+  /**
+   * Menu link tree manipulator that flattens the tree to a single level.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
+   *   The menu link tree to manipulate.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
+   *   The manipulated menu link tree.
+   */
+  public function flatten(array $tree) {
+    foreach ($tree as $key => $element) {
+      if ($tree[$key]->subtree) {
+        $tree += $this->flatten($tree[$key]->subtree);
+      }
+      $tree[$key]->subtree = array();
+    }
+    return $tree;
+  }
+
+  /**
+   * Menu link tree manipulator that extracts a subtree of the active trail.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
+   *   The menu link tree to manipulate.
+   * @param int $level
+   *   The level in the active trail to extract.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
+   *   The manipulated menu link tree.
+   */
+  public function extractSubtreeOfActiveTrail(array $tree, $level) {
+    // Go down the active trail until the right level is reached.
+    while ($level-- > 0 && $tree) {
+      // Loop through the current level's elements  until we find one that is in
+      // the active trail.
+      while ($element = array_shift($tree)) {
+        if ($element->inActiveTrail) {
+          // If the element is in the active trail, we continue in the subtree.
+          $tree = $element->subtree;
+          break;
+        }
+      }
+    }
+    return $tree;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuActiveTrail.php b/core/lib/Drupal/Core/Menu/MenuActiveTrail.php
new file mode 100644
index 0000000..feea82a
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuActiveTrail.php
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuActiveTrail.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Provides the default implementation of the active menu trail service.
+ */
+class MenuActiveTrail implements MenuActiveTrailInterface {
+
+  /**
+   * The menu link plugin manager.
+   *
+   * @var \Drupal\Core\Menu\MenuLinkManagerInterface
+   */
+  protected $menuLinkManager;
+
+  /**
+   * The route match object for the current page.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * Constructs a \Drupal\Core\Menu\MenuActiveTrail object.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager
+   *   The menu link plugin manager.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   A ruote match object for finding the active link.
+   */
+  public function __construct(MenuLinkManagerInterface $menu_link_manager, RouteMatchInterface $route_match) {
+    $this->menuLinkManager = $menu_link_manager;
+    $this->routeMatch = $route_match;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getActiveTrailIds($menu_name) {
+    // Parent ids; used both as key and value to ensure uniqueness.
+    // We always want all the top-level links with parent == ''.
+    $active_trail = array('' => '');
+
+    // If a link in the given menu indeed matches the route, then use it to
+    // complete the active trail.
+    if ($active_link = $this->getActiveLink($menu_name)) {
+      if ($parents = $this->menuLinkManager->getParentIds($active_link->getPluginId())) {
+        $active_trail = $parents + $active_trail;
+      }
+    }
+
+    return $active_trail;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getActiveTrailCacheKey($menu_name) {
+    return 'menu_trail.' . implode('|', $this->getActiveTrailIds($menu_name));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getActiveLink($menu_name = NULL) {
+    // Note: this is a very simple implementation. If you need more control
+    // over the return value, such as matching a prioritized list of menu names,
+    // you should substitute your own implementation for the 'menu.active_trail'
+    // service in the container.
+    $found = NULL;
+
+    $route_name = $this->routeMatch->getRouteName();
+    // On a default (not custom) 403 page the route name is NULL. On a custom
+    // 403 page we will get the route name for that page, so we can consider
+    // it a feature that a relevant menu tree may be displayed.
+    if ($route_name) {
+      $route_parameters = $this->routeMatch->getRawParameters()->all();
+
+      // Load links matching this route.
+      $links = $this->menuLinkManager->loadLinksByRoute($route_name, $route_parameters, $menu_name);
+      // Select the first matching link.
+      if ($links) {
+        $found = reset($links);
+      }
+    }
+    return $found;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuActiveTrailInterface.php b/core/lib/Drupal/Core/Menu/MenuActiveTrailInterface.php
new file mode 100644
index 0000000..50995ba
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuActiveTrailInterface.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuActiveTrailInterface.
+ */
+
+namespace Drupal\Core\Menu;
+
+/**
+ * Defines an interface for the active menu trail service.
+ *
+ * The active trail of a given menu is the trail from the current page to the
+ * root of that menu's tree.
+ */
+interface MenuActiveTrailInterface {
+
+  /**
+   * 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 plugin ids.
+   */
+  public function getActiveTrailIds($menu_name);
+
+  /**
+   * Gets the active trail cache key of the specified menu tree.
+   *
+   * @param string $menu_name
+   *   The menu name of the requested tree.
+   *
+   * @return string
+   *   The cache key that uniquely identifies the active trail of the menu tree.
+   */
+  public function getActiveTrailCacheKey($menu_name);
+
+  /**
+   * Fetches a menu link which matches the route name, parameters and menu name.
+   *
+   * @param string|NULL $menu_name
+   *   (optional) The menu within which to find the active link. If omitted, all
+   *   menus will be searched.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkInterface|NULL
+   *   The menu link for the given route name, parameters and menu, or NULL if
+   *   the current user cannot access the current page (i.e. we have a 403
+   *   response).
+   */
+  public function getActiveLink($menu_name = NULL);
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkBase.php b/core/lib/Drupal/Core/Menu/MenuLinkBase.php
new file mode 100644
index 0000000..9796b84
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkBase.php
@@ -0,0 +1,197 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuLinkBase.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Core\Plugin\PluginBase;
+use Drupal\Core\Url;
+use Drupal\Component\Plugin\Exception\PluginException;
+
+/**
+ * Base class used for MenuLink plugins.
+ */
+abstract class MenuLinkBase extends PluginBase implements MenuLinkInterface {
+
+  /**
+   * Defines the list of definition values where an override is allowed.
+   *
+   * @var array
+   */
+  protected $overrideAllowed = array();
+
+  /**
+   * Returns the weight of the menu link.
+   *
+   * @return int
+   *   The weight of the menu link, 0 by default.
+   */
+  public function getWeight() {
+    // By default the weight is 0.
+    if (!isset($this->pluginDefinition['weight'])) {
+      $this->pluginDefinition['weight'] = 0;
+    }
+    return $this->pluginDefinition['weight'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTitle() {
+    // Subclasses may pull in the request or specific attributes as parameters.
+    $options = array();
+    if (!empty($this->pluginDefinition['title_context'])) {
+      $options['context'] = $this->pluginDefinition['title_context'];
+    }
+    $args = array();
+    if (isset($this->pluginDefinition['title_arguments']) && $title_arguments = $this->pluginDefinition['title_arguments']) {
+      $args = (array) $title_arguments;
+    }
+    return $this->t($this->pluginDefinition['title'], $args, $options);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMenuName() {
+    return $this->pluginDefinition['menu_name'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProvider() {
+    return $this->pluginDefinition['provider'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getParent() {
+    return $this->pluginDefinition['parent'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isHidden() {
+    return (bool) $this->pluginDefinition['hidden'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isExpanded() {
+    return (bool) $this->pluginDefinition['expanded'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isResetable() {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isTranslatable() {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isDeletable() {
+    return (bool) $this->getDeleteRoute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    if ($this->pluginDefinition['description']) {
+      return $this->t($this->pluginDefinition['description']);
+    }
+    return '';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOptions() {
+    return $this->pluginDefinition['options'] ?: array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMetaData() {
+    return $this->pluginDefinition['metadata'] ?: array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getUrlObject($title_attribute = TRUE) {
+    $options = $this->getOptions();
+    $description = $this->getDescription();
+    if ($title_attribute && $description) {
+      $options['attributes']['title'] = $description;
+    }
+    if (empty($this->pluginDefinition['url'])) {
+      return new Url($this->pluginDefinition['route_name'], $this->pluginDefinition['route_parameters'], $options);
+    }
+    else {
+      $url = Url::createFromPath($this->pluginDefinition['url']);
+      $url->setOptions($options);
+      return $url;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormClass() {
+    return $this->pluginDefinition['form_class'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDeleteRoute() {
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEditRoute() {
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTranslateRoute() {
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteLink() {
+    throw new PluginException(sprintf("Menu link plugin with ID %s does not support deletion", $this->getPluginId()));
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkDefault.php b/core/lib/Drupal/Core/Menu/MenuLinkDefault.php
new file mode 100644
index 0000000..5cd52d2
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkDefault.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuLinkDefault.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Default object used for MenuLink plugins.
+ */
+class MenuLinkDefault extends MenuLinkBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $overrideAllowed = array(
+    'menu_name' => 1,
+    'parent' => 1,
+    'weight' => 1,
+    'expanded' => 1,
+    'hidden' => 1,
+  );
+
+  /**
+   * The static menu link service used to store updates to weight/parent etc.
+   *
+   * @var \Drupal\Core\Menu\StaticMenuLinkOverridesInterface
+   */
+  protected $staticOverride;
+
+  /**
+   * Constructs a new MenuLinkDefault.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $static_override
+   *   The static override storage.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, StaticMenuLinkOverridesInterface $static_override) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->staticOverride = $static_override;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('menu_link.static.overrides')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isResetable() {
+    // The link can be reset if it has an override.
+    return $this->staticOverride->loadOverride($this->getPluginId());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateLink(array $new_definition_values, $persist) {
+    $overrides = array_intersect_key($new_definition_values, $this->overrideAllowed);
+    if ($persist) {
+      $this->staticOverride->saveOverride($this->getPluginId(), $overrides);
+    }
+    // Update the definition.
+    $this->pluginDefinition = $overrides + $this->getPluginDefinition();
+    return $this->pluginDefinition;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkInterface.php b/core/lib/Drupal/Core/Menu/MenuLinkInterface.php
new file mode 100644
index 0000000..64f99a2
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkInterface.php
@@ -0,0 +1,218 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuLinkInterface.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Component\Plugin\DerivativeInspectionInterface;
+
+/**
+ * Default object used for LocalTaskPlugins.
+ */
+interface MenuLinkInterface extends PluginInspectionInterface, DerivativeInspectionInterface {
+
+  /**
+   * Returns the weight of the menu link.
+   *
+   * @return int
+   *   The weight of the menu link, 0 by default.
+   */
+  public function getWeight();
+
+  /**
+   * Returns the localized title to be shown for this link.
+   *
+   * @return string
+   *   The title of the menu link.
+   */
+  public function getTitle();
+
+  /**
+   * Returns the description of the menu link.
+   */
+  public function getDescription();
+
+  /**
+   * Returns the menu name of the menu link.
+   */
+  public function getMenuName();
+
+  /**
+   * Returns the provider (module name) of the menu link.
+   */
+  public function getProvider();
+
+  /**
+   * Returns the plugin ID of the menu link's parent, or an empty string.
+   */
+  public function getParent();
+
+  /**
+   * Returns whether the menu link is hidden.
+   *
+   * @return bool
+   *   TRUE for hidden, FALSE otherwise.
+   */
+  public function isHidden();
+
+  /**
+   * Returns whether the child menu links should always been shown.
+   *
+   * @return bool
+   *   TRUE for expanded, FALSE otherwise.
+   */
+  public function isExpanded();
+
+  /**
+   * Returns whether this link can be reset.
+   *
+   * In general, only links that store overrides using the
+   * menu_link.static.overrides service should return TRUE for this method.
+   *
+   * @return bool
+   *   TRUE if it can be reset, FALSE otherwise.
+   */
+  public function isResetable();
+
+  /**
+   * Returns whether this link can be translated.
+   *
+   * @return bool
+   *   TRUE if the link can be translated, FALSE otherwise.
+   */
+  public function isTranslatable();
+
+  /**
+   * Returns whether this link can be deleted.
+   *
+   * @return bool
+   *   TRUE if the link can be deleted, FALSE otherwise.
+   */
+  public function isDeletable();
+
+  /**
+   * Returns a URL object containing either the external path or route.
+   *
+   * @param bool $title_attribute
+   *   If TRUE, add the link description (if present) as the title attribute.
+   *
+   * @return \Drupal\Core\Url
+   *   A a URL object containing either the external path or route.
+   */
+  public function getUrlObject($title_attribute = TRUE);
+
+  /**
+   * Returns the options for this link.
+   *
+   * @return array
+   *   The options for the menu link.
+   */
+  public function getOptions();
+
+  /**
+   * Returns any metadata for this link.
+   *
+   * @return array
+   *   The metadata for the menu link.
+   */
+  public function getMetaData();
+
+  /**
+   * Returns whether the rendered link can be cached.
+   *
+   * The plugin class may make some or all of the data used in the Url object
+   * and build array dynamic.  For example, it could include the current
+   * user name in the title, the current time in the description, or a
+   * destination query string.  In addition the route parameters may
+   * be dynamic so an access check should be performed for each user.
+   *
+   * @return bool
+   *   TRUE if the link can be cached, FALSE otherwise.
+   */
+  public function isCacheable();
+
+  /**
+   * Updates and saves values for a menu link.
+   *
+   * The override is written depending on the implementation.
+   * Static links, for example, have a dedicated override storage service.
+   *
+   * Depending on the implementation details of the class, not all definition
+   * values may be changed. For example, changes to the title of a static
+   * link will be discarded.
+   *
+   * In general, this method should not be called directly, but will be called
+   * automatically from MenuLinkTreeInterface::updateLink()
+   *
+   * @param array $new_definition_values
+   *   The new values for the link definition. This will usually be just a
+   *   subset of the plugin definition.
+   * @param bool $persist
+   *   TRUE to have the link persist the changed values to any additional
+   *   storage.
+   *
+   * @return array
+   *   The plugin definition incorporating any allowed changes.
+   */
+  public function updateLink(array $new_definition_values, $persist);
+
+  /**
+   * Delete a menu link.
+   *
+   * In general, this method should not be called directly, but will be called
+   * automatically from MenuLinkTreeInterface::deleteLink()
+   *
+   * This method will only delete the link from any additional storage, but
+   * not from the menu.link_tree service.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   If the link is not deletable.
+   */
+  public function deleteLink();
+
+  /**
+   * Returns the name of a class that can build an editing form for this link.
+   *
+   * To instantiate the form class, use an instance of the
+   * \Drupal\Core\DependencyInjection\ClassResolverInterface, such as from the
+   * class_resolver service. Then call the setMenuLinkInstance() method
+   * on the form instance with the menu link plugin instance.
+   *
+   * @return string
+   *   A class that implements \Drupal\Core\Menu\Form\MenuLinkFormInterface.
+   */
+  public function getFormClass();
+
+  /**
+   * Returns parameters for a delete link, or an empty value.
+   *
+   * @return array
+   *   Array with keys route_name and route_parameters
+   */
+  public function getDeleteRoute();
+
+  /**
+   * Returns parameters for a custom edit link, or an empty value.
+   *
+   * Plugins should return a value here if they have a special edit form,
+   * or if they need to define additional local tasks, local actions, etc.
+   * that are visible from the edit form.
+   *
+   * @return array
+   *   Array with keys route_name and route_parameters
+   */
+  public function getEditRoute();
+
+  /**
+   * Returns parameters for a translate link, or an empty value.
+   *
+   * @return array
+   *   Array with keys route_name and route_parameters
+   */
+  public function getTranslateRoute();
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkManager.php b/core/lib/Drupal/Core/Menu/MenuLinkManager.php
new file mode 100644
index 0000000..33521fc
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkManager.php
@@ -0,0 +1,385 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuLinkManager.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Component\Plugin\Exception\PluginException;
+use Drupal\Component\Plugin\Exception\PluginNotFoundException;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Component\Utility\String;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
+use Drupal\Core\Plugin\Discovery\YamlDiscovery;
+use Drupal\Core\Plugin\Factory\ContainerFactory;
+
+
+/**
+ * Manages discovery, instantiation, and tree building of menu link plugins.
+ *
+ * This manager finds plugins that are rendered as menu links.
+ */
+class MenuLinkManager implements MenuLinkManagerInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaults = array(
+    // (required) The name of the menu for this link.
+    'menu_name' => 'tools',
+    // (required) The name of the route this links to, unless it's external.
+    'route_name' => '',
+    // Parameters for route variables when generating a link.
+    'route_parameters' => array(),
+    // The external URL if this link has one (required if route_name is empty).
+    'url' => '',
+    // The static title for the menu link.
+    'title' => '',
+    'title_arguments' => array(),
+    'title_context' => '',
+    // The description.
+    'description' => '',
+    // The plugin ID of the parent link (or NULL for a top-level link).
+    'parent' => '',
+    // The weight of the link.
+    'weight' => 0,
+    // The default link options.
+    'options' => array(),
+    'expanded' => 0,
+    'hidden' => 0,
+    // The name of the module providing this link.
+    'provider' => '',
+    'metadata' => array(),
+    // Default class for local task implementations.
+    'class' => 'Drupal\Core\Menu\MenuLinkDefault',
+    'form_class' => 'Drupal\Core\Menu\Form\MenuLinkDefaultForm',
+    // The plugin id. Set by the plugin system based on the top-level YAML key.
+    'id' => '',
+  );
+
+  /**
+   * The object that discovers plugins managed by this manager.
+   *
+   * @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
+   */
+  protected $discovery;
+
+  /**
+   * The object that instantiates plugins managed by this manager.
+   *
+   * @var \Drupal\Component\Plugin\Factory\FactoryInterface
+   */
+  protected $factory;
+
+  /**
+   * The menu link tree storage.
+   *
+   * @var \Drupal\Core\Menu\MenuTreeStorageInterface
+   */
+  protected $treeStorage;
+
+  /**
+   * Service providing overrides for static links
+   *
+   * @var \Drupal\Core\Menu\StaticMenuLinkOverridesInterface
+   */
+  protected $overrides;
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+
+  /**
+   * Constructs a \Drupal\Core\Menu\MenuLinkTree object.
+   *
+   * @param \Drupal\Core\Menu\MenuTreeStorageInterface $tree_storage
+   *   The menu link tree storage.
+   * @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $overrides
+   *   Service providing overrides for static links
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   */
+  public function __construct(MenuTreeStorageInterface $tree_storage, StaticMenuLinkOverridesInterface $overrides, ModuleHandlerInterface $module_handler) {
+    $this->treeStorage = $tree_storage;
+    $this->overrides = $overrides;
+    $this->factory = new ContainerFactory($this);
+    $this->moduleHandler = $module_handler;
+  }
+
+  /**
+   * Performs extra processing on plugin definitions.
+   *
+   * By default we add defaults for the type to the definition. If a type has
+   * additional processing logic they can do that by replacing or extending the
+   * method.
+   */
+  protected function processDefinition(&$definition, $plugin_id) {
+    $definition = NestedArray::mergeDeep($this->defaults, $definition);
+    $definition['parent'] = (string) $definition['parent'];
+    $definition['id'] = $plugin_id;
+  }
+
+  /**
+   * Instantiate if necessary and return a YamlDiscovery instance.
+   *
+   * Since the discovery is very rarely used - only when the rebuild() method
+   * is called - it's instantiated only when actually needed instead of in the
+   * constructor.
+   */
+  protected function getDiscovery() {
+    if (empty($this->discovery)) {
+      $yaml = new YamlDiscovery('menu_links', $this->moduleHandler->getModuleDirectories());
+      $this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml);
+    }
+    return $this->discovery;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefinitions() {
+    // Since this function is called rarely, instantiate the discovery here.
+    $definitions = $this->getDiscovery()->getDefinitions();
+
+    $this->moduleHandler->alter('menu_links_discovered', $definitions);
+
+    foreach ($definitions as $plugin_id => &$definition) {
+      $definition['id'] = $plugin_id;
+      $this->processDefinition($definition, $plugin_id);
+    }
+
+    // If this plugin was provided by a module that does not exist, remove the
+    // plugin definition.
+    foreach ($definitions as $plugin_id => $plugin_definition) {
+      if (!empty($plugin_definition['provider']) && !$this->moduleHandler->moduleExists($plugin_definition['provider'])) {
+        unset($definitions[$plugin_id]);
+      }
+    }
+    return $definitions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function rebuild() {
+    $definitions = $this->getDefinitions();
+    // Apply overrides from config.
+    $overrides = $this->overrides->loadMultipleOverrides(array_keys($definitions));
+    foreach ($overrides as $id => $changes) {
+      if (!empty($definitions[$id])) {
+        $definitions[$id] = $changes + $definitions[$id];
+      }
+    }
+    $this->treeStorage->rebuild($definitions);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefinition($plugin_id, $exception_on_invalid = TRUE) {
+    $definition = $this->treeStorage->load($plugin_id);
+    if (empty($definition) && $exception_on_invalid) {
+      throw new PluginNotFoundException("$plugin_id could not be found.");
+    }
+    return $definition;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasDefinition($plugin_id) {
+    return (bool) $this->getDefinition($plugin_id, FALSE);
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\Core\Menu\MenuLinkInterface
+   *   A menu link instance.
+   */
+  public function createInstance($plugin_id, array $configuration = array()) {
+    return $this->factory->createInstance($plugin_id, $configuration);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInstance(array $options) {
+    if (isset($options['id'])) {
+      return $this->createInstance($options['id']);
+    }
+  }
+
+  /**
+   * Deletes all links for a menu.
+   *
+   * @todo - this should really only be called as part of the flow of
+   * deleting a menu entity, so maybe we should load it and make sure it's
+   * not locked?
+   *
+   * @param string $menu_name
+   *   The name of the menu whose links will be deleted.
+   */
+  public function deleteLinksInMenu($menu_name) {
+    foreach ($this->treeStorage->loadByProperties(array('menu_name' => $menu_name)) as $plugin_id => $definition) {
+      $instance = $this->createInstance($plugin_id);
+      if ($instance->isResetable()) {
+        $new_instance = $this->resetInstance($instance);
+        $affected_menus[$new_instance->getMenuName()] = $new_instance->getMenuName();
+      }
+      elseif ($instance->isDeletable()) {
+        $this->deleteInstance($instance, TRUE);
+      }
+    }
+  }
+
+  /**
+   * Helper function to delete a specific instance.
+   */
+  protected function deleteInstance(MenuLinkInterface $instance, $persist) {
+    $id = $instance->getPluginId();
+    if ($instance->isDeletable()) {
+      if ($persist) {
+        $instance->deleteLink();
+      }
+    }
+    else {
+      throw new PluginException(sprintf("Menu link plugin with ID %s does not support deletion", $id));
+    }
+    $this->treeStorage->delete($id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteLink($id, $persist = TRUE) {
+    $definition = $this->treeStorage->load($id);
+    // It's possible the definition has already been deleted, or doesn't exist.
+    if ($definition) {
+      $instance = $this->createInstance($id);
+      $this->deleteInstance($instance, $persist);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function menuNameInUse($menu_name) {
+    $this->treeStorage->menuNameInUse($menu_name);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function countMenuLinks($menu_name = NULL) {
+    return $this->treeStorage->countMenuLinks($menu_name);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getParentIds($id) {
+    if ($this->getDefinition($id, FALSE)) {
+      return $this->treeStorage->getRootPathIds($id);
+    }
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getChildIds($id) {
+    if ($this->getDefinition($id, FALSE)) {
+      return $this->treeStorage->getAllChildIds($id);
+    }
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadLinksByRoute($route_name, array $route_parameters = array(), $menu_name = NULL) {
+    $instances = array();
+    $loaded = $this->treeStorage->loadByRoute($route_name, $route_parameters, $menu_name);
+    foreach ($loaded as $plugin_id => $definition) {
+      $instances[$plugin_id] = $this->createInstance($plugin_id);
+    }
+    return $instances;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createLink($id, array $definition) {
+    // Add defaults and other stuff, so there is no requirement to specify
+    // everything.
+    $this->processDefinition($definition, $id);
+
+    // Store the new link in the tree.
+    $this->treeStorage->save($definition);
+    return $this->createInstance($id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateLink($id, array $new_definition_values, $persist = TRUE) {
+    $instance = $this->createInstance($id);
+    if ($instance) {
+      $new_definition_values['id'] = $id;
+      $changed_definition = $instance->updateLink($new_definition_values, $persist);
+      $this->treeStorage->save($changed_definition);
+    }
+    return $instance;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function resetLink($id) {
+    $instance = $this->createInstance($id);
+    $new_instance = $this->resetInstance($instance);
+    return $new_instance;
+  }
+
+  /**
+   * Resets the menu link to its default settings.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkInterface $instance
+   *   The menu link which should be reset.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkInterface
+   *   The reset menu link.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   Thrown when the menu link is not resetable.
+   */
+  protected function resetInstance(MenuLinkInterface $instance) {
+    $id = $instance->getPluginId();
+
+    if (!$instance->isResetable()) {
+      throw new PluginException(String::format('Menu link %id is not resetable', array('%id' => $id)));
+    }
+    // Get the original data from disk, reset the override and re-save the menu
+    // tree for this link.
+    $definition = $this->getDefinitions()[$id];
+    $this->overrides->deleteOverride($id);
+    $this->treeStorage->save($definition);
+    return $this->createInstance($id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function resetDefinitions() {
+    $this->treeStorage->resetDefinitions();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkManagerInterface.php b/core/lib/Drupal/Core/Menu/MenuLinkManagerInterface.php
new file mode 100644
index 0000000..94e565d
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkManagerInterface.php
@@ -0,0 +1,168 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuLinkTreeInterface.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Component\Plugin\PluginManagerInterface;
+
+/**
+ * Defines an interface for creating menu links and retrieving menu link trees.
+ */
+interface MenuLinkManagerInterface extends PluginManagerInterface {
+
+  /**
+   * Trigger discovery, save, and cleanup of static links.
+   */
+  public function rebuild();
+
+  /**
+   * Deletes or resets all links for a menu.
+   *
+   * @param string $menu_name
+   *   The name of the menu whose links will be deleted or reset.
+   */
+  public function deleteLinksInMenu($menu_name);
+
+  /**
+   * Deletes a single link from the menu tree.
+   *
+   * @param string $id
+   *   The menu link plugin ID.
+   * @param bool $persist
+   *   If TRUE, this method will attempt to persist the deletion from any
+   *   external storage by invoking MenuLinkInterface::deleteLink() on
+   *   the plugin that is being deleted.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   If the $id is not valid, existing, plugin ID or if the link cannot be
+   *   deleted.
+   */
+  public function deleteLink($id, $persist = TRUE);
+
+  /**
+   * Load multiple plugin instances based on route.
+   *
+   * @param string $route_name
+   *   The route name.
+   * @param array $route_parameters
+   *   (optional) The route parameters, defaults to an empty array.
+   * @param string $menu_name
+   *   (optional) Restricts the found links to just those in the named menu.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkInterface[]
+   *   An array of instances keyed by ID.
+   */
+  public function loadLinksByRoute($route_name, array $route_parameters = array(), $menu_name = NULL);
+
+  /**
+   * Adds a new link to the tree storage.
+   *
+   * Use this function in case you know there is no entry in the tree. This is
+   * the case if you don't use plugin definition to fill in the tree.
+   *
+   * @param string $id
+   *   The menu link plugin ID.
+   * @param array $definition
+   *   The values of the link.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkInterface
+   *   The updated menu link instance.
+   */
+  public function createLink($id, array $definition);
+
+  /**
+   * Updates the values for a menu link in the tree storage.
+   *
+   * @param string $id
+   *   The menu link plugin ID.
+   * @param array $new_definition_values
+   *   The new values for the link definition. This will usually be just a
+   *   subset of the plugin definition.
+   * @param bool $persist
+   *   TRUE to also have the link instance itself persist the changed values
+   *   to any additional storage by invoking MenuLinkInterface::updateLink() on
+   *   the plugin that is being updated.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkInterface
+   *   The updated menu link instance.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   If the $id is not valid, existing, plugin ID.
+   */
+  public function updateLink($id, array $new_definition_values, $persist = TRUE);
+
+  /**
+   * Resets the values for a menu link based on the values found by discovery.
+   *
+   * @param string $id
+   *   The menu link plugin ID.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkInterface
+   *   The menu link instance after being reset.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   If the $id is not valid, existing, plugin ID or if the link cannot be
+   *   reset.
+   */
+  public function resetLink($id);
+
+  /**
+   * Counts the total number of menu links.
+   *
+   * @param string $menu_name
+   *   (optional) The menu name to count by, defaults to NULL.
+   */
+  public function countMenuLinks($menu_name = NULL);
+
+  /**
+   * Loads all parent link IDs of a given menu link.
+   *
+   * This method is very similar to getActiveTrailIds() but allows the link
+   * to be specified rather than being discovered based on the menu name
+   * and request. This method is mostly useful for testing.
+   *
+   * @param string $id
+   *   The menu link plugin ID.
+   *
+   * @return array
+   *   An ordered array of IDs representing the path to the root of the tree.
+   *   The first element of the array will be equal to $id, unless $id is not
+   *   valid, in which case the return value will be NULL.
+   */
+  public function getParentIds($id);
+
+  /**
+   * Loads all child link IDs of a given menu link, regardless of visibility.
+   *
+   * This method is mostly useful for testing.
+   *
+   * @param string $id
+   *   The menu link plugin ID.
+   *
+   * @return array
+   *   An unordered array of IDs representing the IDs of all children, or NULL
+   *   if the ID is invalid.
+   */
+  public function getChildIds($id);
+
+  /**
+   * Determine if any links use a given menu name.
+   *
+   * @param string $menu_name
+   *   The menu name.
+   *
+   * @return bool
+   *   TRUE if any links are present in the named menu, FALSE otherwise.
+   */
+  public function menuNameInUse($menu_name);
+
+  /**
+   * Resets any local definition cache. Used for testing.
+   */
+  public function resetDefinitions();
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTree.php b/core/lib/Drupal/Core/Menu/MenuLinkTree.php
new file mode 100644
index 0000000..a0f3416
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkTree.php
@@ -0,0 +1,228 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuLinkTree.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Controller\ControllerResolverInterface;
+use Drupal\Core\Routing\RouteProviderInterface;
+
+/**
+ * Provides loading, transforming and rendering of menu link trees.
+ */
+class MenuLinkTree implements MenuLinkTreeInterface {
+
+  /**
+   * The menu link tree storage.
+   *
+   * @var \Drupal\Core\Menu\MenuTreeStorageInterface
+   */
+  protected $treeStorage;
+
+  /**
+   * Service providing overrides for static links
+   *
+   * @var \Drupal\Core\Menu\StaticMenuLinkOverridesInterface
+   */
+  protected $overrides;
+
+  /**
+   * The route provider to load routes by name.
+   *
+   * @var \Drupal\Core\Routing\RouteProviderInterface
+   */
+  protected $routeProvider;
+
+  /**
+   * The active menu trail service.
+   *
+   * @var \Drupal\Core\Menu\MenuActiveTrailInterface
+   */
+  protected $menuActiveTrail;
+
+  /**
+   * The controller resolver.
+   *
+   * @var \Drupal\Core\Controller\ControllerResolverInterface
+   */
+  protected $controllerResolver;
+
+  /**
+   * Constructs a \Drupal\Core\Menu\MenuLinkTree object.
+   *
+   * @param \Drupal\Core\Menu\MenuTreeStorageInterface $tree_storage
+   *   The menu link tree storage.
+   * @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager
+   *   The menu link plugin manager.
+   * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
+   *   The route provider to load routes by name.
+   * @param \Drupal\Core\Menu\MenuActiveTrailInterface $menu_active_trail
+   *   The active menu trail service.
+   * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
+   *   The controller resolver.
+   */
+  public function __construct(MenuTreeStorageInterface $tree_storage, MenuLinkManagerInterface $menu_link_manager, RouteProviderInterface $route_provider, MenuActiveTrailInterface $menu_active_trail, ControllerResolverInterface $controller_resolver) {
+    $this->treeStorage = $tree_storage;
+    $this->menuLinkManager = $menu_link_manager;
+    $this->routeProvider = $route_provider;
+    $this->menuActiveTrail = $menu_active_trail;
+    $this->controllerResolver = $controller_resolver;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCurrentRouteMenuTreeParameters($menu_name) {
+    $active_trail = $this->menuActiveTrail->getActiveTrailIds($menu_name);
+
+    $parameters = new MenuTreeParameters();
+    $parameters->setActiveTrail($active_trail)
+      // We want links in the active trail to be expanded.
+      ->addExpandedParents($active_trail)
+      // We marked the links in the active trail to be expanded, but we also
+      // want their descendants that have the "expanded" flag enabled to be
+      // expanded.
+      ->addExpandedParents($this->treeStorage->getExpanded($menu_name, $active_trail));
+
+    return $parameters;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function load($menu_name, MenuTreeParameters $parameters) {
+    $data = $this->treeStorage->loadTreeData($menu_name, $parameters);
+    // Pre-load all the route objects in the tree for access checks.
+    if ($data['route_names']) {
+      $this->routeProvider->getRoutesByNames($data['route_names']);
+    }
+    return $this->createInstances($data['tree']);
+  }
+
+  /**
+   * Helper function that recursively instantiates the plugins.
+   */
+  protected function createInstances($data_tree) {
+    $tree = array();
+    foreach ($data_tree as $key => $element) {
+      $subtree = $this->createInstances($element['subtree']);
+      // Build a MenuLinkTreeElement out of the menu tree link definition:
+      // transform the tree link definition into a link definition and store
+      // tree metadata.
+      $tree[$key] = new MenuLinkTreeElement(
+        $this->menuLinkManager->createInstance($element['definition']['id']),
+        (bool) $element['has_children'],
+        (int) $element['depth'],
+        (bool) $element['in_active_trail'],
+        $subtree
+      );
+    }
+    return $tree;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function transform(array $tree, array $manipulators) {
+    foreach ($manipulators as $manipulator) {
+      $callable = $manipulator['callable'];
+      if (!is_callable($callable)) {
+        $callable = $this->controllerResolver->getControllerFromDefinition($callable);
+      }
+      // Prepare the arguments for the menu tree manipulator callable; the first
+      // argument is always the menu link tree.
+      if (isset($manipulator['args'])) {
+        array_unshift($manipulator['args'], $tree);
+        $tree = call_user_func_array($callable, $manipulator['args']);
+      }
+      else {
+        $tree = call_user_func($callable, $tree);
+      }
+    }
+    return $tree;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build(array $tree) {
+    $build = array();
+
+    foreach ($tree as $data) {
+      $class = array();
+      /** @var \Drupal\Core\Menu\MenuLinkInterface $link */
+      $link = $data->link;
+      // Generally we only deal with visible links, but just in case.
+      if ($link->isHidden()) {
+        continue;
+      }
+      // Set a class for the <li>-tag. Only set 'expanded' class if the link
+      // also has visible children within the current tree.
+      if ($data->hasChildren && !empty($data->subtree)) {
+        $class[] = 'expanded';
+      }
+      elseif ($data->hasChildren) {
+        $class[] = 'collapsed';
+      }
+      else {
+        $class[] = 'leaf';
+      }
+      // Set a class if the link is in the active trail.
+      if ($data->inActiveTrail) {
+        $class[] = 'active-trail';
+      }
+
+      // Allow menu-specific theme overrides.
+      $element['#theme'] = 'menu_link__' . strtr($link->getMenuName(), '-', '_');
+      $element['#attributes']['class'] = $class;
+      $element['#title'] = $link->getTitle();
+      $element['#url'] = $link->getUrlObject();
+      $element['#below'] = $data->subtree ? $this->build($data->subtree) : array();
+      if (isset($data->options)) {
+        $element['#url']->setOptions(NestedArray::mergeDeep($element['#url']->getOptions(), $data->options));
+      }
+      $element['#original_link'] = $link;
+      // Index using the link's unique ID.
+      $build[$link->getPluginId()] = $element;
+    }
+    if ($build) {
+      // Make sure drupal_render() does not re-order the links.
+      $build['#sorted'] = TRUE;
+      // Get the menu name from the last link.
+      $menu_name = $link->getMenuName();
+      // Add the theme wrapper for outer markup.
+      // Allow menu-specific theme overrides.
+      $build['#theme_wrappers'][] = 'menu_tree__' . strtr($menu_name, '-', '_');
+      // Set cache tag.
+      $build['#cache']['tags']['menu'][$menu_name] = $menu_name;
+    }
+
+    return $build;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function maxDepth() {
+    return $this->treeStorage->maxDepth();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSubtreeHeight($id) {
+    return $this->treeStorage->getSubtreeHeight($id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getExpanded($menu_name, array $parents) {
+    return $this->treeStorage->getExpanded($menu_name, $parents);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTreeElement.php b/core/lib/Drupal/Core/Menu/MenuLinkTreeElement.php
new file mode 100644
index 0000000..db5cbc9
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkTreeElement.php
@@ -0,0 +1,125 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuLinkTreeElement.
+ */
+
+namespace Drupal\Core\Menu;
+
+/**
+ * Provides a value object to model an element in a menu link tree.
+ *
+ * \Drupal\Core\Menu\MenuLinkTreeInterface objects represent a menu link's data.
+ * Objects of this class provide complimentary data: the placement in a tree.
+ * Therefore, we can summarize this split as follows:
+ * - Menu link objects contain all information about an individual menu link,
+ *   plus what their parent is. But they don't know where exactly in a menu link
+ *   tree they live.
+ * - Instances of this class are complimentary to those objects, they know:
+ *   1. all additional metadata from {menu_tree}, which contains "materialized"
+ *      metadata about a menu link tree, such as whether a link in the tree has
+ *      visible children and the depth relative to the root;
+ *   2. plus all additional metadata that's adjusted for the current tree query,
+ *      such as whether the link is in the active trail, whether the link is
+ *      accessible for the current user, and the link's children (which are only
+ *      loaded if the link was marked as "expanded" by the query).
+ *
+ * @see \Drupal\Core\Menu\MenuTreeStorage::loadTreeData()
+ */
+class MenuLinkTreeElement {
+
+  /**
+   * The menu link for this element in a menu link tree.
+   *
+   * @var \Drupal\Core\Menu\MenuLinkInterface|array
+   */
+  public $link;
+
+  /**
+   * The subtree of this element in the menu link tree (this link's children).
+   *
+   * (Children of a link are only loaded if a link is marked as "expanded" by
+   * the query.)
+   *
+   * @var \Drupal\Core\Menu\MenuLinkTreeElement[]
+   */
+  public $subtree;
+
+  /**
+   * The depth of this link relative to the root of the tree.
+   *
+   * @var int
+   */
+  public $depth;
+
+  /**
+   * Whether this link has any children at all.
+   *
+   * @var bool
+   */
+  public $hasChildren;
+
+  /**
+   * Whether this link is in the active trail.
+   *
+   * @var bool
+   */
+  public $inActiveTrail;
+
+  /**
+   * Whether this link is accessible by the current user.
+   *
+   * @var bool|NULL
+   */
+  public $access;
+
+  /**
+   * Additional options for this link.
+   *
+   * This is merged (\Drupal\Component\Utility\NestedArray::mergeDeep()) with
+   * \Drupal\Core\Menu\MenuLinkInterface::getOptions(), to allow menu link tree
+   * manipulators to add or override link options.
+   */
+  public $options = array();
+
+  /**
+   * Constructs a new MenuLinkTreeElement.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkInterface $link
+   *   The menu link for this element in the menu link tree.
+   * @param bool $has_children
+   *   A flag as to whether this element has children even if they are not
+   *   included in the tree (i.e. this may be TRUE even if $subtree is empty).
+   * @param int $depth
+   *   The depth of this element relative to the tree root.
+   * @param bool $in_active_trail
+   *   A flag as to whether this link was included in the list of active trail
+   *  IDs used to build the tree.
+   * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $subtree
+   *   The children of this element in the menu link tree.
+   */
+  public function __construct($link, $has_children, $depth, $in_active_trail, array $subtree) {
+    // Essential properties.
+    $this->link = $link;
+    $this->hasChildren = $has_children;
+    $this->depth = $depth;
+    $this->subtree = $subtree;
+    $this->inActiveTrail = $in_active_trail;
+  }
+
+  /**
+   * Counts all menu links in the current subtree.
+   *
+   * @return int
+   *   The number of menu links in this subtree (one plus the number of menu
+   *   links in all descendants).
+   */
+  public function count() {
+    $sum = function ($carry, MenuLinkTreeElement $element) {
+      return $carry + $element->count();
+    };
+    return 1 + array_reduce($this->subtree, $sum);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php b/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php
new file mode 100644
index 0000000..9b1f5df
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php
@@ -0,0 +1,142 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuLinkTreeInterface.
+ */
+
+namespace Drupal\Core\Menu;
+
+/**
+ * Defines an interface for retrieving menu link trees.
+ */
+/**
+ * Defines an interface for loading, transforming and rendering menu link trees.
+ *
+ * The main goal of this service is to, given a menu name, load (::load()) the
+ * corresponding tree of menu links and turning this list of menu links into a
+ * tree (by looking at their tree metadata). Because menu links themselves are
+ * responsible for translation, this will already be translated for the current
+ * language.
+ * Which links are loaded can be specified in the menu link tree parameters that
+ * passed to ::load(). You can build your own set of parameter, but you can also
+ * start from a typical default (::getCurrentRouteMenuTreeParameters()).
+ *
+ * @see \Drupal\Core\Menu\MenuLinkTreeParameters
+ *
+ * If desired, one can transform (::transform()) that tree of menu links, for
+ * example performing access checking (to only show those links that can be
+ * accessed by the end user) or adding custom classes to links (to show icons
+ * next to the links). Very complex tasks can be performed as well (such as
+ * extracting a subtree from the menu link tree depending on the active trail).
+ * These transformations are performed by "menu link tree manipulators", and
+ * they can be used to perform any kind of transformation imaginable.
+ *
+ * @see \Drupal\menu_link\DefaultMenuTreeManipulators
+ *
+ * Finally, if desired, that tree of menu links can be built into a renderable
+ * array (::build()) for rendering as HTML.
+ */
+interface MenuLinkTreeInterface {
+
+  /**
+   * The default menu link tree parameters for rendering a menu.
+   *
+   * Builds menu link tree parameters that:
+   * - expand all links in the active trail based on route being viewed
+   * - also expands the descendants of the links in the active trail whose
+   *   'expanded' flag is enabled
+   *
+   * This only sets the (relatively complex) parameters to achieve the two above
+   * goals, but you can still further customize these parameters.
+   *
+   * @see \Drupal\Core\Menu\MenuLinkTreeParameters
+   *
+   * @param string $menu_name
+   *   The menu name, needed for retrieving the active trail and links with the
+   *   'expanded' flag enabled.
+   *
+   * @return \Drupal\Core\Menu\MenuTreeParameters
+   *   The parameters to determine which menu links to be loaded into a tree.
+   */
+  public function getCurrentRouteMenuTreeParameters($menu_name);
+
+  /**
+   * Loads a menu tree with a menu link plugin instance at each element.
+   *
+   * @param string $menu_name
+   *   The name of the menu.
+   * @param \Drupal\Core\Menu\MenuTreeParameters $parameters
+   *   The parameters to determine which menu links to be loaded into a tree.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
+   *   A menu link tree.
+   */
+  public function load($menu_name, MenuTreeParameters $parameters);
+
+  /**
+   * Applies menu link tree manipulators to transform the given tree.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
+   *   The menu tree to manipulate.
+   * @param array $manipulators
+   *   The menu link tree manipulators to apply. Each is an array with keys:
+   *   - callable: a callable or a string that can be resolved to a callable
+   *               by ControllerResolverInterface::getControllerFromDefinition()
+   *   - args: optional array of arguments to pass to the callable after $tree.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkTreeElement[]
+   *   The manipulated menu link tree.
+   */
+  public function transform(array $tree, array $manipulators);
+
+  /**
+   * Builds a renderable array of a 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 \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
+   *   A data structure representing the tree as returned from ::load().
+   *
+   * @return array
+   *   A renderable array.
+   */
+  public function build(array $tree);
+
+  /**
+   * The maximum depth of tree that is supported.
+   *
+   * @return int
+   *   The maximum depth.
+   */
+  public function maxDepth();
+
+  /**
+   * Finds the height of a subtree rooted by of the given ID.
+   *
+   * @param string $id
+   *   The the ID of an item in the storage.
+   *
+   * @return int
+   *   Returns the height of the subtree. This will be at least 1 if the ID
+   *   exists, or 0 if the ID does not exist in the storage.
+   */
+  public function getSubtreeHeight($id);
+
+  /**
+   * Find expanded links in a menu given a set of possible parents.
+   *
+   * @param string $menu_name
+   *   The menu name.
+   * @param array $parents
+   *   One or more parent IDs to match.
+   *
+   * @return array
+   *   The menu link IDs that are flagged as expanded in this menu.
+   */
+  public function getExpanded($menu_name, array $parents);
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuParentFormSelector.php b/core/lib/Drupal/Core/Menu/MenuParentFormSelector.php
new file mode 100644
index 0000000..ae22631
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuParentFormSelector.php
@@ -0,0 +1,165 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuParentFormSelector.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Component\Utility\Unicode;
+
+
+class MenuParentFormSelector implements MenuParentFormSelectorInterface {
+
+  /**
+   * The menu link tree service.
+   *
+   * @var \Drupal\Core\Menu\MenuLinkTreeInterface
+   */
+  protected $menuLinkTree;
+
+  /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface
+   */
+  protected $entityManager;
+
+  /**
+   * Constructs a \Drupal\Core\Menu\MenuParentFormSelector
+   *
+   * @param MenuLinkTreeInterface $menu_link_tree
+   *   The menu link tree service.
+   * @param EntityManagerInterface $entity_manager
+   *   The entity manager.
+   */
+  public function __construct(MenuLinkTreeInterface $menu_link_tree, EntityManagerInterface $entity_manager) {
+    $this->menuLinkTree = $menu_link_tree;
+    $this->entityManager = $entity_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getParentSelectOptions($id = '', array $menus = NULL) {
+    if (!isset($menus)) {
+      $menus = $this->getMenuOptions();
+    }
+
+    $options = array();
+    $depth_limit = $this->getParentDepthLimit($id);
+    foreach ($menus as $menu_name => $menu_title) {
+      $options[$menu_name . ':'] = '<' . $menu_title . '>';
+
+      $parameters = new MenuTreeParameters();
+      $parameters->setMaxDepth($depth_limit);
+      $tree = $this->menuLinkTree->load($menu_name, $parameters);
+      $manipulators = array(
+        array('callable' => 'menu.default_tree_manipulators:checkAccess'),
+        array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'),
+      );
+      $tree = $this->menuLinkTree->transform($tree, $manipulators);
+      $this->parentSelectOptionsTreeWalk($tree, $menu_name, '--', $options, $id, $depth_limit);
+    }
+    return $options;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function parentSelectElement($menu_parent, $id = '', array $menus = NULL) {
+    $options = $this->getParentSelectOptions($id, $menus);
+    // If no options were found, there is nothing to select.
+    if ($options) {
+      if (!isset($options[$menu_parent])) {
+        // Try putting it at the top level in the current menu.
+        list($menu_name, $parent) = explode(':', $menu_parent, 2);
+        $menu_parent = $menu_name . ':';
+      }
+      if (isset($options[$menu_parent])) {
+        return array(
+          '#type' => 'select',
+          '#options' => $options,
+          '#default_value' => $menu_parent,
+        );
+      }
+    }
+    return array();
+  }
+
+  /**
+   * Returns the maximum depth of the possible parents of the menu link.
+   *
+   * @param string $id
+   *   The menu link plugin ID or an empty value for a new link.
+   *
+   * @return int
+   *   The depth related to the depth of the given menu link.
+   */
+  protected function getParentDepthLimit($id) {
+    if ($id) {
+      $limit = $this->menuLinkTree->maxDepth() - $this->menuLinkTree->getSubtreeHeight($id);
+    }
+    else {
+      $limit = $this->menuLinkTree->maxDepth() - 1;
+    }
+    return $limit;
+  }
+
+  /**
+   * Iterates over all items in the tree to prepare the parents select options.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
+   *   The menu tree.
+   * @param string $menu_name
+   *   The menu name.
+   * @param string $indent
+   *   The indentation string used for the label.
+   * @param array $options
+   *   The select options.
+   * @param string $exclude
+   *   An excluded menu link.
+   * @param int $depth_limit
+   *   The maximum depth of menu links considered for the select options.
+   */
+  protected function parentSelectOptionsTreeWalk(array $tree, $menu_name, $indent, array &$options, $exclude, $depth_limit) {
+    foreach ($tree as $element) {
+      if ($element->depth > $depth_limit) {
+        // Don't iterate through any links on this level.
+        break;
+      }
+      $link = $element->link;
+      if ($link->getPluginId() != $exclude) {
+        $title = $indent . ' ' . Unicode::truncate($link->getTitle(), 30, TRUE, FALSE);
+        if ($link->isHidden()) {
+          $title .= ' (' . t('disabled') . ')';
+        }
+        $options[$menu_name . ':' . $link->getPluginId()] = $title;
+        if (!empty($element->subtree)) {
+          $this->parentSelectOptionsTreeWalk($element->subtree, $menu_name, $indent . '--', $options, $exclude, $depth_limit);
+        }
+      }
+    }
+  }
+
+  /**
+   * Gets a list of menu names for use as options.
+   *
+   * @param array $menu_names
+   *   Optional array of menu names to limit the options, or NULL to load all.
+   *
+   * @return array
+   *   Keys are menu names (ids) values are the menu labels.
+   */
+  protected function getMenuOptions(array $menu_names = NULL) {
+    $menus = $this->entityManager->getStorage('menu')->loadMultiple($menu_names);
+    $options = array();
+    foreach ($menus as $menu) {
+      $options[$menu->id()] = $menu->label();
+    }
+    return $options;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuParentFormSelectorInterface.php b/core/lib/Drupal/Core/Menu/MenuParentFormSelectorInterface.php
new file mode 100644
index 0000000..fdcc4b6
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuParentFormSelectorInterface.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuParentFormSelectorInterface.
+ */
+
+namespace Drupal\Core\Menu;
+
+
+interface MenuParentFormSelectorInterface {
+
+  /**
+   * Gets the options for a select element to choose a menu and parent.
+   *
+   * @param string $id
+   *   Optional ID of a link plugin. This will exclude the link and its
+   *   children from the select options.
+   * @param array $menus
+   *   Optional array of menu names as keys and titles as values to limit
+   *   the select options.  If NULL, all menus will be included.
+   *
+   * @return array
+   *   Keyed array where the keys are contain a menu name and parent ID and
+   *   the values are a menu name or link title indented by depth.
+   *
+   * @deprecated
+   */
+  public function getParentSelectOptions($id = '', array $menus = NULL);
+
+  /**
+   * Get a form element to choose a menu and parent.
+   *
+   * The specific type of form element will vary depending on the
+   * implementation, but callers will normally need to set the #title for the
+   * element.
+   *
+   * @param string $menu_parent
+   *   A menu name and parent ID concatenated with a ':' character to use as the
+   *   default value.
+   * @param string $id
+   *   Optional ID of a link plugin. This will exclude the link and its
+   *   children from being selected.
+   * @param array $menus
+   *   Optional array of menu names as keys and titles as values to limit
+   *   the values that may be selected. If NULL, all menus will be included.
+   *
+   * @return array
+   *   A form element to choose a parent, or an empty array if no possible
+   *   parents exist for the given parameters. The resulting form value will be
+   *   a single string containing the chosen menu name and parent ID separated
+   *   by a ':' character.
+   */
+  public function parentSelectElement($menu_parent, $id = '', array $menus = NULL);
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuTreeParameters.php b/core/lib/Drupal/Core/Menu/MenuTreeParameters.php
new file mode 100644
index 0000000..d8adb40
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuTreeParameters.php
@@ -0,0 +1,218 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuTreeParameters.
+ */
+
+namespace Drupal\Core\Menu;
+
+/**
+ * Provides a value object to model menu tree parameters.
+ *
+ * Menu tree parameters are used to determine the set of definitions to be
+ * loaded from \Drupal\Core\Menu\MenuTreeStorageInterface. Hence they determine
+ * the shape and content of the tree:
+ * - which parent IDs should be used to restrict the tree, i.e. only links with
+ *   a parent in the list will be included.
+ * - which menu links are omitted, i.e. minimum and maximum depth
+ *
+ * @todo - add getter methods and make all properties protected.
+ * @todo - define an interface instead of using the concrete class to type hint.
+ */
+class MenuTreeParameters {
+
+  /**
+   * A menu link plugin ID that should be used as the root.
+   *
+   * By default the "real" root (@code '' @encode) of a menu is used. But, when
+   * only the descendants (subtree) of a certain menu link are needed, a custom
+   * root can be specified.
+   *
+   * @var string
+   */
+  public $root = '';
+
+  /**
+   * The minimum depth of menu links in the resulting tree. Root-relative.
+   *
+   * Defaults to 1, which is the default to build a whole tree for a menu
+   * (excluding the root).
+   *
+   * @var int|null
+   */
+  public $minDepth = NULL;
+
+  /**
+   * The maximum depth of menu links in the resulting tree. Root-relative.
+   *
+   * @var int|null
+   */
+  public $maxDepth = NULL;
+
+  /**
+   * An array of parent link IDs. This restricts the tree to only menu links
+   * that are at the top level or have a parent ID in this list. If empty, the
+   * whole menu tree is built.
+   *
+   * @var string[]
+   */
+  public $expandedParents = array();
+
+  /**
+   * An array of menu link plugin IDs, representing the trail from the currently
+   * active menu link to the ("real") root of that menu link's menu.  This does
+   * not affect the way the tree is built, it only is used to set the value of
+   * the inActiveTrail property for each tree element.
+   *
+   * Defaults to the empty array.
+   *
+   * @var string[]
+   */
+  public $activeTrail = array();
+
+  /**
+   * An associative array of custom query condition key/value pairs to restrict
+   * the links loaded.
+   *
+   * Defaults to the empty array.
+   *
+   * @var array
+   */
+  public $conditions = array();
+
+  /**
+   * Sets a root; loads a menu tree with this menu link plugin ID as root.
+   *
+   * @param string $root
+   *   A menu link plugin ID, or @code '' @endcode to use the "real" root.
+   *
+   * @return $this
+   *
+   * @codeCoverageIgnore
+   */
+  public function setRoot($root) {
+    $this->root = (string) $root;
+    return $this;
+  }
+
+  /**
+   * Sets a minimum depth; loads a menu tree from the given level.
+   *
+   * @param int $min_depth
+   *   The (root-relative) minimum depth to apply.
+   *
+   * @return $this
+   */
+  public function setMinDepth($min_depth) {
+    $this->minDepth = max(1, $min_depth);
+    return $this;
+  }
+
+  /**
+   * Sets a minimum depth; loads a menu tree up to the given level.
+   *
+   * @param int $max_depth
+   *   The (root-relative) maximum depth to apply.
+   *
+   * @return $this
+   *
+   * @codeCoverageIgnore
+   */
+  public function setMaxDepth($max_depth) {
+    $this->maxDepth = $max_depth;
+    return $this;
+  }
+
+  /**
+   * Adds parent menu links IDs to restrict the tree (only show children).
+   *
+   * @param string[] $parents
+   *   An array containing the parent IDs to limit the tree.
+   *
+   * @return $this
+   */
+  public function addExpandedParents(array $parents) {
+    $this->expandedParents = array_merge($this->expandedParents, $parents);
+    $this->expandedParents = array_unique($this->expandedParents);
+    return $this;
+  }
+
+  /**
+   * Sets the active trail IDs used to set the inActiveTrail property.
+   *
+   * @param string[] $active_trail
+   *   An array containing the active trail: a list of menu link plugin IDs.
+   *
+   * @return $this
+   *
+   * @see \Drupal\Core\Menu\MenuActiveTrail::getActiveTrailIds()
+   *
+   * @codeCoverageIgnore
+   */
+  public function setActiveTrail(array $active_trail) {
+    $this->activeTrail = $active_trail;
+    return $this;
+  }
+
+  /**
+   * Adds a custom query condition.
+   *
+   * @param string $definition_field
+   *   Only conditions that are testing menu link definition fields are allowed.
+   * @param mixed $value
+   *   The value to test the link definition field against. In most cases, this
+   *   is a scalar. For more complex options, it is an array. The meaning of
+   *   each element in the array is dependent on the $operator.
+   * @param string|NULL $operator
+   *   The comparison operator, such as =, <, or >=. It also accepts more
+   *   complex options such as IN, LIKE, or BETWEEN.
+   *
+   * @return $this
+   */
+  public function addCondition($definition_field, $value, $operator = NULL) {
+    if (!isset($operator)) {
+      $this->conditions[$definition_field] = $value;
+    }
+    else {
+      $this->conditions[$definition_field] = array($value, $operator);
+    }
+    return $this;
+  }
+
+  /**
+   * Excludes hidden links.
+   *
+   * @return $this
+   */
+  public function excludeHiddenLinks() {
+    $this->addCondition('hidden', 0);
+    return $this;
+  }
+
+  /**
+   * Ensures only the top level of the tree is loaded.
+   *
+   * @return $this
+   */
+  public function topLevelOnly() {
+    $this->setMaxDepth(1);
+    return $this;
+  }
+
+  /**
+   * Excludes the root menu link from the tree.
+   *
+   * Note that this is only necessary when you specified a custom root, because
+   * the "real" root (@code '' @encode) is mapped to a non-existing menu link.
+   * Hence when loading a menu link tree without specifying a custom root, you
+   * will never get a root; the tree will start at the children.
+   *
+   * @return $this
+   */
+  public function excludeRoot() {
+    $this->setMinDepth(1);
+    return $this;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuTreeStorage.php b/core/lib/Drupal/Core/Menu/MenuTreeStorage.php
new file mode 100644
index 0000000..1c94d54
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuTreeStorage.php
@@ -0,0 +1,1403 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuTreeStorage.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\Component\Plugin\Exception\PluginException;
+use Drupal\Core\Database\SchemaObjectExistsException;
+use Drupal\Core\Database\Database;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheBackendInterface;
+
+/**
+ * Provides a tree storage using the database.
+ */
+class MenuTreeStorage implements MenuTreeStorageInterface {
+
+  /**
+   * The maximum depth of a menu links tree.
+   */
+  const MAX_DEPTH = 9;
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * Cache backend instance for the extracted tree data.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $menuCacheBackend;
+
+  /**
+   * The database table name.
+   *
+   * @var string
+   */
+  protected $table;
+
+  /**
+   * Additional database connection options to use in queries.
+   *
+   * @var array
+   */
+  protected $options = array();
+
+  /**
+   * Stores definitions that have already been loaded for better performance.
+   */
+  protected $definitions = array();
+
+  /**
+   * List of serialized fields.
+   *
+   * @var array
+   */
+  protected $serializedFields;
+
+  /**
+   * List of plugin definition fields.
+   *
+   * @todo - inject this from the plugin manager?
+   *
+   * @var array
+   */
+  protected $definitionFields = array(
+    'menu_name',
+    'route_name',
+    'route_parameters',
+    'url',
+    'title',
+    'title_arguments',
+    'title_context',
+    'description',
+    'parent',
+    'weight',
+    'options',
+    'expanded',
+    'hidden',
+    'provider',
+    'metadata',
+    'class',
+    'form_class',
+    'id',
+  );
+
+  /**
+   * Constructs a new \Drupal\Core\Menu\MenuTreeStorage.
+   *
+   * @param \Drupal\Core\Database\Connection $connection
+   *   A Database connection to use for reading and writing configuration data.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $menu_cache_backend
+   *   Cache backend instance for the extracted tree data.
+   * @param string $table
+   *   A database table name to store configuration data in.
+   * @param array $options
+   *   (optional) Any additional database connection options to use in queries.
+   */
+  public function __construct(Connection $connection, CacheBackendInterface $menu_cache_backend, $table, array $options = array()) {
+    $this->connection = $connection;
+    $this->menuCacheBackend = $menu_cache_backend;
+    $this->table = $table;
+    $this->options = $options;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function maxDepth() {
+    return static::MAX_DEPTH;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function resetDefinitions() {
+    $this->definitions = array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function rebuild(array $definitions) {
+    $links = array();
+    $children = array();
+    $top_links = array();
+    // Fetch the list of existing menus, in case some are not longer populated
+    // after the rebuild.
+    $before_menus = $this->getMenuNames();
+    if ($definitions) {
+      foreach ($definitions as $id => $link) {
+        // Flag this link as discovered, i.e. saved via rebuild().
+        $link['discovered'] = 1;
+        if (!empty($link['parent'])) {
+          $children[$link['parent']][$id] = $id;
+        }
+        else {
+          // A top level link - we need them to root our tree.
+          $top_links[$id] = $id;
+          $link['parent'] = '';
+        }
+        $links[$id] = $link;
+      }
+    }
+    foreach ($top_links as $id) {
+      $this->saveRecursive($id, $children, $links);
+    }
+    // Handle any children we didn't find starting from top-level links.
+    foreach ($children as $orphan_links) {
+      foreach ($orphan_links as $id) {
+        // Force it to the top level.
+        $links[$id]['parent'] = '';
+        $this->saveRecursive($id, $children, $links);
+      }
+    }
+    // Find any previously discovered menu links that no longer exist.
+    if ($definitions) {
+      $query = $this->connection->select($this->table, NULL, $this->options);
+      $query->addField($this->table, 'id');
+      $query->condition('discovered', 1);
+      $query->condition('id', array_keys($definitions), 'NOT IN');
+      $query->orderBy('depth', 'DESC');
+      $result = $query->execute()->fetchCol();
+    }
+    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) {
+      $this->purgeMultiple($result);
+    }
+    $this->resetDefinitions();
+    $affected_menus = $this->getMenuNames() + $before_menus;
+    // Invalidate any cache tagged with any menu name.
+    Cache::invalidateTags(array('menu' => $affected_menus));
+    $this->resetDefinitions();
+    // @todo - this is probably unneeded.
+    $this->menuCacheBackend->invalidateAll();
+  }
+
+  /**
+   * Purges multiple menu links that no longer exist.
+   *
+   * @param array $ids
+   *   An array of menu link IDs.
+   * @param bool $prevent_reparenting
+   *   (optional) Disables the re-parenting logic from the deletion process.
+   *   Defaults to FALSE.
+   */
+  protected function purgeMultiple(array $ids, $prevent_reparenting = FALSE) {
+    if (!$prevent_reparenting) {
+      $loaded = $this->loadFullMultiple($ids);
+      foreach ($loaded as $id => $link) {
+        if ($link['has_children']) {
+          $children = $this->loadByProperties(array('parent' => $id));
+          foreach ($children as $child) {
+            $child['parent'] = $link['parent'];
+            $this->save($child);
+          }
+        }
+      }
+    }
+    $query = $this->connection->delete($this->table, $this->options);
+    $query->condition('id', $ids, 'IN');
+    $query->execute();
+  }
+
+  /**
+   * Executes a select query while making sure the database table exists.
+   *
+   * @param \Drupal\Core\Database\Query\SelectInterface $query
+   *   The select object to be executed.
+   *
+   * @return \Drupal\Core\Database\StatementInterface|null
+   *   A prepared statement, or NULL if the query is not valid.
+   *
+   * @throws \Exception
+   *   If the table could not be created or the database connection failed.
+   */
+  protected function safeExecuteSelect(SelectInterface $query) {
+    try {
+      return $query->execute();
+    }
+    catch (\Exception $e) {
+      // If there was an exception, try to create the table.
+      if ($this->ensureTableExists()) {
+        return $query->execute();
+      }
+      // Some other failure that we can not recover from.
+      throw $e;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function save(array $link) {
+    $affected_menus = $this->doSave($link);
+    $this->resetDefinitions();
+    Cache::invalidateTags(array('menu' => $affected_menus));
+    return $affected_menus;
+  }
+
+  /**
+   * Helper function for rebuild that saves a link without clearing caches.
+   *
+   * @param array $link
+   *   A definition for a \Drupal\Core\Menu\MenuLinkInterface plugin.
+   *
+   * @return array
+   *   The names of the menus affected by the save operation (1 or 2).
+   *
+   * @throws \Exception
+   *   If the storage back-end does not exist and could not be created.
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   If the definition is invalid - for example, if the specified parent
+   *   would cause the links children to be moved to greater than the maximum
+   *   depth.
+   */
+  protected function doSave(array $link) {
+    $original = $this->loadFull($link['id']);
+    // @todo - should we just return here if the link values match the original
+    //   values completely?.
+    $affected_menus = array();
+
+    $transaction = $this->connection->startTransaction();
+    try {
+      if ($original) {
+        $link['mlid'] = $original['mlid'];
+        $link['has_children'] = $original['has_children'];
+        $affected_menus[$original['menu_name']] = $original['menu_name'];
+      }
+      else {
+        // Generate a new mlid.
+        $options = array('return' => Database::RETURN_INSERT_ID) + $this->options;
+        $link['mlid'] = $this->connection->insert($this->table, $options)
+          ->fields(array('id' => $link['id'], 'menu_name' => $link['menu_name']))
+          ->execute();
+      }
+      $fields = $this->preSave($link, $original);
+      // We may be moving the link to a new menu.
+      $affected_menus[$fields['menu_name']] = $fields['menu_name'];
+      $query = $this->connection->update($this->table, $this->options);
+      $query->condition('mlid', $link['mlid']);
+      $query->fields($fields)
+        ->execute();
+      if ($original) {
+        $this->updateParentalStatus($original);
+      }
+      $this->updateParentalStatus($link);
+    }
+    catch (\Exception $e) {
+      $transaction->rollback();
+      throw $e;
+    }
+    return $affected_menus;
+  }
+
+  /**
+   * Using the link definition, but up all the fields needed for database save.
+   *
+   * @param array $link
+   *   The link definition to be updated.
+   * @param array $original
+   *   The link definition before the changes. May be empty if not found.
+   *
+   * @return array
+   *   The values which will be stored.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   Thrown when the specific depth exceeds the maximum.
+   */
+  protected function preSave(array &$link, array $original) {
+    static $schema_fields, $schema_defaults;
+    if (empty($schema_fields)) {
+      $schema = static::schemaDefinition();
+      $schema_fields = $schema['fields'];
+      foreach ($schema_fields as $name => $spec) {
+        if (isset($spec['default'])) {
+          $schema_defaults[$name] = $spec['default'];
+        }
+      }
+    }
+
+    // Try to find a parent link. If found, assign it and derive its menu.
+    $parent = $this->findParent($link, $original);
+    if ($parent) {
+      $link['parent'] = $parent['id'];
+      $link['menu_name'] = $parent['menu_name'];
+    }
+    else {
+      $link['parent'] = '';
+    }
+
+    // If no corresponding parent link was found, move the link to the
+    // top-level.
+    foreach ($schema_defaults as $name => $default) {
+      if (!isset($link[$name])) {
+        $link[$name] = $default;
+      }
+    }
+    $fields = array_intersect_key($link, $schema_fields);
+    asort($fields['route_parameters']);
+    // Since this will be urlencoded, it's safe to store and match against a
+    // text field.
+    $fields['route_param_key'] = $fields['route_parameters'] ? UrlHelper::buildQuery($fields['route_parameters']) : '';
+
+    foreach ($this->serializedFields() as $name) {
+      $fields[$name] = serialize($fields[$name]);
+    }
+
+    // Directly fill parents for top-level links.
+    if (empty($link['parent'])) {
+      $fields['p1'] = $link['mlid'];
+      for ($i = 2; $i <= $this->maxDepth(); $i++) {
+        $fields["p$i"] = 0;
+      }
+      $fields['depth'] = 1;
+    }
+    // Otherwise, ensure that this link's depth is not beyond the maximum depth
+    // and fill parents based on the parent link.
+    else {
+      // @todo - we want to also check $original['has_children'] here, but that
+      //   will be 0 even if there are children if those are hidden.
+      //   has_children is really just the rendering hint. So, we either need
+      //   to define another column (has_any_children), or do the extra query.
+      if ($original) {
+        $limit = $this->maxDepth() - $this->doFindChildrenRelativeDepth($original) - 1;
+      }
+      else {
+        $limit = $this->maxDepth() - 1;
+      }
+      if ($parent['depth'] > $limit) {
+        throw new PluginException(sprintf('The link with ID %s or its children exceeded the maximum depth of %d', $link['id'], $this->maxDepth()));
+      }
+      $this->setParents($fields, $parent);
+    }
+
+    // Need to check both parent and menu_name, since parent can be empty in any
+    // menu.
+    if ($original && ($link['parent'] != $original['parent'] || $link['menu_name'] != $original['menu_name'])) {
+      $this->moveChildren($fields, $original);
+    }
+    // We needed the mlid above, but not in the update query.
+    unset($fields['mlid']);
+
+    // Cast booleans to int, if needed.
+    $fields['hidden'] = (int) $fields['hidden'];
+    $fields['expanded'] = (int) $fields['expanded'];
+    return $fields;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function delete($id) {
+    // Children get re-attached to the menu link's parent.
+    $item = $this->loadFull($id);
+    // It's possible the link is already deleted.
+    if ($item) {
+      $parent = $item['parent'];
+      $children = $this->loadByProperties(array('parent' => $id));
+      foreach ($children as $child) {
+        $child['parent'] = $parent;
+        $this->save($child);
+      }
+
+      $this->connection->delete($this->table, $this->options)
+        ->condition('id', $id)
+        ->execute();
+
+      $this->updateParentalStatus($item);
+      // Many children may have moved.
+      $this->resetDefinitions();
+      Cache::invalidateTags(array('menu' => $item['menu_name']));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSubtreeHeight($id) {
+    $original = $this->loadFull($id);
+    return $original ? $this->doFindChildrenRelativeDepth($original) + 1 : 0;
+  }
+
+  /**
+   * Finds the relative depth of this link's deepest child.
+   *
+   * @param array $original
+   *   The parent definition used to find the depth.
+   *
+   * @return int
+   *   Returns the relative depth.
+   */
+  protected function doFindChildrenRelativeDepth(array $original) {
+    $query = $this->connection->select($this->table, $this->options);
+    $query->addField($this->table, 'depth');
+    $query->condition('menu_name', $original['menu_name']);
+    $query->orderBy('depth', 'DESC');
+    $query->range(0, 1);
+
+    for ($i = 1; $i <= static::MAX_DEPTH && $original["p$i"]; $i++) {
+      $query->condition("p$i", $original["p$i"]);
+    }
+
+    $max_depth = $this->safeExecuteSelect($query)->fetchField();
+
+    return ($max_depth > $original['depth']) ? $max_depth - $original['depth'] : 0;
+  }
+
+  /**
+   * Sets the materialized path field values based on the parent.
+   *
+   * @param array $fields
+   *   The menu link.
+   * @param array $parent
+   *   The parent menu link.
+   */
+  protected function setParents(array &$fields, array $parent) {
+    $fields['depth'] = $parent['depth'] + 1;
+    $i = 1;
+    while ($i < $fields['depth']) {
+      $p = 'p' . $i++;
+      $fields[$p] = $parent[$p];
+    }
+    $p = 'p' . $i++;
+    // The parent (p1 - p9) corresponding to the depth always equals the mlid.
+    $fields[$p] = $fields['mlid'];
+    while ($i <= static::MAX_DEPTH) {
+      $p = 'p' . $i++;
+      $fields[$p] = 0;
+    }
+  }
+
+  /**
+   * Moves the link's children using the query fields value and original values.
+   *
+   * @param array $fields
+   *   The changed menu link.
+   * @param array $original
+   *   The original menu link.
+   */
+  protected function moveChildren($fields, $original) {
+    $query = $this->connection->update($this->table, $this->options);
+
+    $query->fields(array('menu_name' => $fields['menu_name']));
+
+    $expressions = array();
+    for ($i = 1; $i <= $fields['depth']; $i++) {
+      $expressions[] = array("p$i", ":p_$i", array(":p_$i" => $fields["p$i"]));
+    }
+    $j = $original['depth'] + 1;
+    while ($i <= $this->maxDepth() && $j <= $this->maxDepth()) {
+      $expressions[] = array('p' . $i++, 'p' . $j++, array());
+    }
+    while ($i <= $this->maxDepth()) {
+      $expressions[] = array('p' . $i++, 0, array());
+    }
+
+    $shift = $fields['depth'] - $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', $original['menu_name']);
+
+    for ($i = 1; $i <= $this->maxDepth() && $original["p$i"]; $i++) {
+      $query->condition("p$i", $original["p$i"]);
+    }
+
+    $query->execute();
+  }
+
+  /**
+   * Loads the parent definition if it exists.
+   *
+   * @param array $link
+   *   The link definition to check.
+   * @param array|FALSE $original
+   *   The original link, or FALSE.
+   *
+   * @return array|FALSE
+   *   Returns a definition array, or FALSE if no parent was found.
+   */
+  protected function findParent($link, $original) {
+    $parent = FALSE;
+
+    // This item is explicitly top-level, skip the rest of the parenting.
+    if (isset($link['parent']) && empty($link['parent'])) {
+      return $parent;
+    }
+
+    // If we have a parent link ID, try to use that.
+    $candidates = array();
+    if (isset($link['parent'])) {
+      $candidates[] = $link['parent'];
+    }
+    elseif (!empty($original['parent']) && $link['menu_name'] == $original['menu_name']) {
+      // Otherwise, fall back to the original parent.
+      $candidates[] = $original['parent'];
+    }
+
+    foreach ($candidates as $id) {
+      $parent = $this->loadFull($id);
+      if ($parent) {
+        break;
+      }
+    }
+    return $parent;
+  }
+
+  /**
+   * Set the has_children flag for the link's parent if it has visible children.
+   *
+   * @param array $link
+   *   The link to get a parent ID from.
+   */
+  protected function updateParentalStatus(array $link) {
+    // If parent is empty, there is nothing to update.
+    if (!empty($link['parent'])) {
+      // Check if at least one visible child exists in the table.
+      $query = $this->connection->select($this->table, $this->options);
+      $query->addExpression('1');
+      $query->range(0, 1);
+      $query
+        ->condition('menu_name', $link['menu_name'])
+        ->condition('parent', $link['parent'])
+        ->condition('hidden', 0);
+
+      $parent_has_children = ((bool) $query->execute()->fetchField()) ? 1 : 0;
+      $this->connection->update($this->table, $this->options)
+        ->fields(array('has_children' => $parent_has_children))
+        ->condition('id', $link['parent'])
+        ->execute();
+    }
+  }
+
+  /**
+   * Prepare a link by unserializing values and saving the definition.
+   *
+   * @param array $link
+   *   The data loaded in the query.
+   * @param bool $intersect
+   *   If TRUE, filter out values that are not part of the actual definition.
+   * @return array
+   *   The prepared link data.
+   */
+  protected function prepareLink(array $link, $intersect = FALSE) {
+    foreach ($this->serializedFields() as $name) {
+      $link[$name] = unserialize($link[$name]);
+    }
+    if ($intersect) {
+      $link = array_intersect_key($link, array_flip($this->definitionFields()));
+    }
+    $this->definitions[$link['id']] = $link;
+    return $link;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadByProperties(array $properties) {
+    // @todo - only allow loading by plugin definition properties.
+    $query = $this->connection->select($this->table, $this->options);
+    $query->fields($this->table, $this->definitionFields());
+    foreach ($properties as $name => $value) {
+      $query->condition($name, $value);
+    }
+    $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
+    foreach ($loaded as $id => $link) {
+      $loaded[$id] = $this->prepareLink($link);
+    }
+    return $loaded;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadByRoute($route_name, array $route_parameters = array(), $menu_name = NULL) {
+    asort($route_parameters);
+    // Since this will be urlencoded, it's safe to store and match against a
+    // text field.
+    // @todo - does this make more sense than using the system path?
+    $param_key = $route_parameters ? UrlHelper::buildQuery($route_parameters) : '';
+    $query = $this->connection->select($this->table, $this->options);
+    $query->fields($this->table, $this->definitionFields());
+    $query->condition('route_name', $route_name);
+    $query->condition('route_param_key', $param_key);
+    if ($menu_name) {
+      $query->condition('menu_name', $menu_name);
+    }
+    // Make the ordering deterministic.
+    $query->orderBy('depth');
+    $query->orderBy('weight');
+    $query->orderBy('id');
+    $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
+    foreach ($loaded as $id => $link) {
+      $loaded[$id] = $this->prepareLink($link);
+    }
+    return $loaded;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadMultiple(array $ids) {
+    $query = $this->connection->select($this->table, $this->options);
+    $query->fields($this->table, $this->definitionFields());
+    $query->condition('id', $ids, 'IN');
+    $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
+    foreach ($loaded as $id => $link) {
+      $loaded[$id] = $this->prepareLink($link);
+    }
+    return $loaded;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function load($id) {
+    if (isset($this->definitions[$id])) {
+      return $this->definitions[$id];
+    }
+    $loaded = $this->loadMultiple(array($id));
+    return isset($loaded[$id]) ? $loaded[$id] : FALSE;
+  }
+
+  /**
+   * Loads all table fields, not just those that are in the plugin definition.
+   *
+   * @param string $id
+   *   The menu link ID.
+   *
+   * @return array
+   *   The loaded menu link definition or an empty array if not be found.
+   */
+  protected function loadFull($id) {
+    $loaded = $this->loadFullMultiple(array($id));
+    return isset($loaded[$id]) ? $loaded[$id] : array();
+  }
+
+  /**
+   * Loads multiple menu link definitions by ID.
+   *
+   * @param array $ids
+   *   The IDs to load.
+   *
+   * @return array
+   *   The loaded menu link definitions.
+   */
+  protected function loadFullMultiple(array $ids) {
+    $query = $this->connection->select($this->table, $this->options);
+    $query->fields($this->table);
+    $query->condition('id', $ids, 'IN');
+    $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
+    foreach ($loaded as &$link) {
+      foreach ($this->serializedFields() as $name) {
+        $link[$name] = unserialize($link[$name]);
+      }
+    }
+    return $loaded;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRootPathIds($id) {
+    $subquery = $this->connection->select($this->table, $this->options);
+    // @todo - consider making this dynamic based on static::MAX_DEPTH
+    //   or from the schema if that is generated using static::MAX_DEPTH.
+    $subquery->fields($this->table, array('p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9'));
+    $subquery->condition('id', $id);
+    $result = current($subquery->execute()->fetchAll(\PDO::FETCH_ASSOC));
+    $ids = array_filter($result);
+    if ($ids) {
+      $query = $this->connection->select($this->table, $this->options);
+      $query->fields($this->table, array('id'));
+      $query->orderBy('depth', 'DESC');
+      $query->condition('mlid', $ids, 'IN');
+      // @todo - cache this result in memory if we find it's being used more
+      //   than once per page load.
+      return $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0);
+    }
+    return array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getExpanded($menu_name, array $parents) {
+    // @todo - go back to tracking in state or some other way
+    //   which menus have expanded links?
+    do {
+      $query = $this->connection->select($this->table, $this->options);
+      $query->fields($this->table, array('id'));
+      $query->condition('menu_name', $menu_name);
+      $query->condition('expanded', 1);
+      $query->condition('has_children', 1);
+      $query->condition('hidden', 0);
+      $query->condition('parent', $parents, 'IN');
+      $query->condition('id', $parents, 'NOT IN');
+      $result = $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0);
+      $parents += $result;
+    } while (!empty($result));
+    return $parents;
+  }
+
+  /**
+   * Saves menu links recursively.
+   */
+  protected function saveRecursive($id, &$children, &$links) {
+
+    if (!empty($links[$id]['parent']) && empty($links[$links[$id]['parent']])) {
+      // Invalid parent ID, so remove it.
+      $links[$id]['parent'] = '';
+    }
+    $this->doSave($links[$id]);
+
+    if (!empty($children[$id])) {
+      foreach ($children[$id] as $next_id) {
+        $this->saveRecursive($next_id, $children, $links);
+      }
+    }
+    // Remove processed link names so we can find stragglers.
+    unset($children[$id]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadTreeData($menu_name, MenuTreeParameters $parameters) {
+    // Build the cache id; sort 'expanded' and 'conditions' to prevent duplicate
+    // cache items.
+    sort($parameters->expandedParents);
+    sort($parameters->conditions);
+    // @todo - may be able to skip hashing after https://drupal.org/node/2224847
+    $tree_cid = "tree-data:$menu_name:" . hash('sha256', serialize($parameters));
+    $cache = $this->menuCacheBackend->get($tree_cid);
+    if ($cache && isset($cache->data)) {
+      $data = $cache->data;
+      // Cache the definitions in memory so they don't need to be loaded again.
+      $this->definitions += $data['definitions'];
+      unset($data['definitions']);
+    }
+    else {
+      $links = $this->loadLinks($menu_name, $parameters);
+      $data['tree'] = $this->doBuildTreeData($links, $parameters->activeTrail, $parameters->minDepth);
+      $data['definitions'] = array();
+      $data['route_names'] = $this->collectRoutesAndDefinitions($data['tree'], $data['definitions']);
+      $this->menuCacheBackend->set($tree_cid, $data, Cache::PERMANENT, array('menu' => $menu_name));
+      // The definitions were already added to $this->definitions in
+      // $this->doBuildTreeData()
+      unset($data['definitions']);
+    }
+    return $data;
+  }
+
+  /**
+   * Loads links in the given menu, according to the given tree parameters.
+   *
+   * @param string $menu_name
+   *   A menu name.
+   * @param MenuTreeParameters $parameters
+   *   The parameters to determine which menu links to be loaded into a tree.
+   *   ::loadLinks() will set the absolute minimum depth, which is used
+   *   ::doBuildTreeData().
+   *
+   * @return array
+   *   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_tree} table. This array must be ordered
+   *   depth-first.
+   */
+  protected function loadLinks($menu_name, MenuTreeParameters $parameters) {
+    $query = $this->connection->select($this->table, $this->options);
+    $query->fields($this->table);
+
+    // Allow a custom root to be specified for loading a menu link tree. If
+    // ommitted, the default root (i.e. the actual root, '') is used.
+    if ($parameters->root !== '') {
+      $root = $this->loadFull($parameters->root);
+
+      // If the custom root does not exist, we cannot load the links below it.
+      if (!$root) {
+        return array();
+      }
+
+      // When specifying a custom root, we only want to find links whose
+      // parent IDs match that of the root; that's how ignore the rest of the
+      // tree. In other words: we exclude everything unreachable from the
+      // custom root.
+      for ($i = 1; $i <= $root['depth']; $i++) {
+        $query->condition("p$i", $root["p$i"]);
+      }
+
+      // When specifying a custom root, the menu is determined by that root.
+      $menu_name = $root['menu_name'];
+
+      // If the custom root exists, then we must rewrite some of our
+      // parameters; parameters are relative to the root (default or custom),
+      // but the queries require absolute numbers, so adjust correspondingly.
+      if (isset($parameters->minDepth)) {
+        $parameters->minDepth += $root['depth'];
+      }
+      else {
+        $parameters->minDepth = $root['depth'];
+      }
+      if (isset($parameters->maxDepth)) {
+        $parameters->maxDepth += $root['depth'];
+      }
+    }
+
+    // If no minimum depth is specified, then set the actual minimum depth,
+    // depending on the root.
+    if (!isset($parameters->minDepth)) {
+      if ($parameters->root !== '' && $root) {
+        $parameters->minDepth = $root['depth'];
+      }
+      else {
+        $parameters->minDepth = 1;
+      }
+    }
+
+    for ($i = 1; $i <= $this->maxDepth(); $i++) {
+      $query->orderBy('p' . $i, 'ASC');
+    }
+
+    $query->condition('menu_name', $menu_name);
+
+    if (!empty($parameters->expandedParents)) {
+      $query->condition('parent', $parameters->expandedParents, 'IN');
+    }
+    if (isset($parameters->minDepth) && $parameters->minDepth > 1) {
+      $query->condition('depth', $parameters->minDepth, '>=');
+    }
+    if (isset($parameters->maxDepth)) {
+      $query->condition('depth', $parameters->maxDepth, '<=');
+    }
+    // Add custom query conditions, if any were passed.
+    if (!empty($parameters->conditions)) {
+      // Only allow conditions that are testing definition fields.
+      $parameters->conditions = array_intersect_key($parameters->conditions, array_flip($this->definitionFields()));
+      foreach ($parameters->conditions as $column => $value) {
+        if (!is_array($value)) {
+          $query->condition($column, $value);
+        }
+        else {
+          $operator = $value[1];
+          $value = $value[0];
+          $query->condition($column, $value, $operator);
+        }
+      }
+    }
+
+    $links = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
+
+    return $links;
+  }
+
+  /**
+   * Traverses the menu tree and collects all the route names and definitions.
+   *
+   * @param array $tree
+   *   The menu tree you wish to operate on.
+   * @param array $definitions
+   *   An array to accumulate definitions.
+   *
+   * @return array
+   *   Array of route names, with all values being unique.
+   */
+  protected function collectRoutesAndDefinitions(array $tree, array &$definitions) {
+    return array_values($this->doCollectRoutesAndDefinitions($tree, $definitions));
+  }
+
+  /**
+   * Recursive helper function to collect all the route names and definitions.
+   *
+   * @param array $tree
+   *   The menu link tree.
+   * @param array &$definitions
+   *   The collected definitions.
+   *
+   * @return array
+   *   The collected route names.
+   */
+  protected function doCollectRoutesAndDefinitions(array $tree, array &$definitions) {
+    $route_names = array();
+    foreach (array_keys($tree) as $id) {
+      $definitions[$id] = $this->definitions[$id];
+      if (!empty($definition['route_name'])) {
+        $route_names[$definition['route_name']] = $definition['route_name'];
+      }
+      if ($tree[$id]['subtree']) {
+        $route_names += $this->doCollectRoutesAndDefinitions($tree[$id]['subtree'], $definitions);
+      }
+    }
+    return $route_names;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadSubtreeData($id, $max_relative_depth = NULL) {
+    $tree = array();
+    $root = $this->loadFull($id);
+    if (!$root) {
+      return $tree;
+    }
+    $parameters = new MenuTreeParameters();
+    $parameters->setRoot($id)->excludeHiddenLinks();
+    return $this->loadTreeData($root['menu_name'], $parameters);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function menuNameInUse($menu_name) {
+    $query = $this->connection->select($this->table, $this->options);
+    $query->addField($this->table, 'mlid');
+    $query->condition('menu_name', $menu_name);
+    $query->range(0, 1);
+    return (bool) $this->safeExecuteSelect($query);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMenuNames() {
+    $query = $this->connection->select($this->table, $this->options);
+    $query->addField($this->table, 'menu_name');
+    $query->distinct();
+    return $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function countMenuLinks($menu_name = NULL) {
+    $query = $this->connection->select($this->table, $this->options);
+    if ($menu_name) {
+      $query->condition('menu_name', $menu_name);
+    }
+    return $this->safeExecuteSelect($query->countQuery())->fetchField();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getAllChildIds($id) {
+    $root = $this->loadFull($id);
+    if (!$root) {
+      return array();
+    }
+    $query = $this->connection->select($this->table, $this->options);
+    $query->fields($this->table, array('id'));
+    $query->condition('menu_name', $root['menu_name']);
+    for ($i = 1; $i <= $root['depth']; $i++) {
+      $query->condition("p$i", $root["p$i"]);
+    }
+    // The next p column should not be empty. This excludes the root link.
+    $query->condition("p$i", 0, '>');
+    return $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadAllChildren($id, $max_relative_depth = NULL) {
+    $parameters = new MenuTreeParameters();
+    $parameters->setRoot($id)->excludeRoot()->setMaxDepth($max_relative_depth)->excludeHiddenLinks();
+    $links = $this->loadLinks(NULL, $parameters);
+    foreach ($links as $id => $link) {
+      $links[$id] = $this->prepareLink($link);
+    }
+    return $links;
+  }
+
+  /**
+   * 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_tree} table. This array must be ordered
+   *   depth-first.
+   *   See ::loadTreeData() 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
+   *   The fully built tree.
+   */
+  protected function treeDataRecursive(array &$links, array $parents, $depth) {
+    $tree = array();
+    while ($tree_link_definition = array_pop($links)) {
+      $tree[$tree_link_definition['id']] = array(
+        'definition' => $this->prepareLink($tree_link_definition, TRUE),
+        'has_children' => $tree_link_definition['has_children'],
+        // We need to determine if we're on the path to root so we can later
+        // build the correct active trail.
+        'in_active_trail' => in_array($tree_link_definition['id'], $parents),
+        'subtree' => array(),
+        'depth' => $tree_link_definition['depth'],
+      );
+      // 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[$tree_link_definition['id']]['subtree'] = $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;
+  }
+
+  /**
+   * Checks if the tree table exists and create it if not.
+   *
+   * @return bool
+   *   TRUE if the table was created, FALSE otherwise.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   If a database error occurs.
+   */
+  protected function ensureTableExists() {
+    try {
+      if (!$this->connection->schema()->tableExists($this->table)) {
+        $this->connection->schema()->createTable($this->table, static::schemaDefinition());
+        return TRUE;
+      }
+    }
+    catch (SchemaObjectExistsException $e) {
+      // If another process has already created the config table, attempting to
+      // recreate it will throw an exception. In this case just catch the
+      // exception and do nothing.
+      return TRUE;
+    }
+    catch (\Exception $e) {
+      throw new PluginException($e->getMessage(), NULL, $e);
+    }
+    return FALSE;
+  }
+
+  /**
+   * Helper function to determine serialized fields.
+   */
+  protected function serializedFields() {
+    // For now, build the list from the schema since it's in active development.
+    if (empty($this->serializedFields)) {
+      $schema = static::schemaDefinition();
+      foreach ($schema['fields'] as $name => $field) {
+        if (!empty($field['serialize'])) {
+          $this->serializedFields[] = $name;
+        }
+      }
+    }
+    return $this->serializedFields;
+  }
+
+  /**
+   * Helper function to determine fields that are part of the plugin definition.
+   */
+  protected function definitionFields() {
+    return $this->definitionFields;
+  }
+
+  /**
+   * Defines the schema for the tree table.
+   */
+  protected static function schemaDefinition() {
+    $schema = array(
+      'description' => 'Contains the menu tree hierarchy.',
+      '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,
+        ),
+        'id' => array(
+          'description' => 'Unique machine name: the plugin ID.',
+          'type' => 'varchar',
+          'length' => 255,
+          'not null' => TRUE,
+        ),
+        'parent' => array(
+          'description' => 'The plugin ID for the parent of this link.',
+          'type' => 'varchar',
+          'length' => 255,
+          'not null' => TRUE,
+          'default' => '',
+        ),
+        'route_name' => array(
+          'description' => 'The machine name of a defined Symfony Route this menu item represents.',
+          'type' => 'varchar',
+          'length' => 255,
+        ),
+        'route_param_key' => array(
+          'description' => 'An encoded string of route parameters for loading by route.',
+          '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,
+        ),
+        'url' => array(
+          'description' => 'The external path this link points to (when not using a route).',
+          'type' => 'varchar',
+          'length' => 255,
+          'not null' => TRUE,
+          'default' => '',
+        ),
+        'title' => array(
+          'description' => 'The text displayed for the link.',
+          'type' => 'varchar',
+          'length' => 255,
+          'not null' => TRUE,
+          'default' => '',
+        ),
+        'title_arguments' => array(
+          'description' => 'A serialized array of arguments to be passed to t() (if this plugin uses it).',
+          'type' => 'blob',
+          'size' => 'big',
+          'not null' => FALSE,
+          'serialize' => TRUE,
+        ),
+        'title_context' => array(
+          'description' => 'The translation context for the link title.',
+          'type' => 'varchar',
+          'length' => 255,
+          'not null' => TRUE,
+          'default' => '',
+        ),
+        'description' => array(
+          'description' => 'The description of this link - used for admin pages and title attribute.',
+          'type' => 'text',
+          'not null' => FALSE,
+        ),
+        'class' => array(
+          'description' => 'The class for this link plugin.',
+          'type' => 'text',
+          'not null' => FALSE,
+        ),
+        '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',
+          'size' => 'big',
+          'not null' => FALSE,
+          'serialize' => TRUE,
+        ),
+        'provider' => 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, 0 = a normal, visible link)',
+          'type' => 'int',
+          'not null' => TRUE,
+          'default' => 0,
+          'size' => 'small',
+        ),
+        'discovered' => array(
+          'description' => 'A flag for whether the link was discovered, so can be purged on rebuild',
+          '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,
+        ),
+        'metadata' => array(
+          'description' => 'A serialized array of data that may be used by the plugin instance.',
+          'type' => 'blob',
+          'size' => 'big',
+          'not null' => FALSE,
+          'serialize' => TRUE,
+        ),
+        'has_children' => array(
+          'description' => 'Flag indicating whether any non-hidden links have this link as a parent (1 = children exist, 0 = no children).',
+          'type' => 'int',
+          'not null' => TRUE,
+          'default' => 0,
+          'size' => 'small',
+        ),
+        'depth' => array(
+          'description' => 'The depth relative to the top level. A link with empty parent will have depth == 1.',
+          '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 parent link mlid. 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,
+        ),
+        'form_class' => array(
+          'description' => 'meh',
+          'type' => 'varchar',
+          'length' => 255,
+        ),
+      ),
+      'indexes' => array(
+        'menu_parents' => array(
+          'menu_name',
+          'p1',
+          'p2',
+          'p3',
+          'p4',
+          'p5',
+          'p6',
+          'p7',
+          'p8',
+          'p9',
+        ),
+        // @todo - test this index for effectiveness.
+        'menu_parent_expand_child' => array(
+          'menu_name', 'expanded',
+          'has_children',
+          array('parent', 16),
+        ),
+        'route_values' => array(
+          array('route_name', 32),
+          array('route_param_key', 16),
+        ),
+      ),
+      'primary key' => array('mlid'),
+      'unique keys' => array(
+        'id' => array('id'),
+      ),
+    );
+
+    return $schema;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php b/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php
new file mode 100644
index 0000000..0de1fe6
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php
@@ -0,0 +1,252 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuTreeStorageInterface.
+ */
+
+namespace Drupal\Core\Menu;
+
+interface MenuTreeStorageInterface {
+
+  /**
+   * The maximum depth of tree the storage implementation supports.
+   *
+   * @return int
+   *   The maximum depth.
+   */
+  public function maxDepth();
+
+  /**
+   * Helper function for testing. Clears all definitions cached in memory.
+   */
+  public function resetDefinitions();
+
+  /**
+   * Rebuilds the stored menu link definitions.
+   *
+   * Links that saved by passing definitions into this method must be included
+   * on all future calls, or they will be purged. This allows for automatic
+   * cleanup e.g. when modules are uninstalled.
+   *
+   * @param array $definitions
+   *   The new menu link definitions.
+   *
+   * @todo give this a better name.
+   */
+  public function rebuild(array $definitions);
+
+  /**
+   * Loads a menu link plugin definition from the storage.
+   *
+   * @param string $id
+   *   The menu link plugin ID.
+   *
+   * @return array|FALSE
+   *   Menu Link definition
+   */
+  public function load($id);
+
+  /**
+   * Loads multiple plugin definitions from the storage.
+   *
+   * @param array $ids
+   *   An array of plugin IDs.
+   *
+   * @return array
+   *   An array of menu Link definitions.
+   */
+  public function loadMultiple(array $ids);
+
+  /**
+   * Loads multiple plugin definitions from the storage based on properties.
+   *
+   * @param array $properties
+   *   The properties to filter by.
+   *
+   * @return array
+   *   An array of menu link definitions.
+   */
+  public function loadByProperties(array $properties);
+
+  /**
+   * Loads multiple plugin definitions from the storage based on route.
+   *
+   * @param string $route_name
+   *   The route name.
+   * @param array $route_parameters
+   *   (optional) The route parameters, defaults to an empty array.
+   * @param string $menu_name
+   *   (optional) Restricts the found links to just those in the named menu.
+   *
+   * @return array
+   *   An array of menu link definitions keyed by ID and ordered by depth.
+   */
+  public function loadByRoute($route_name, array $route_parameters = array(), $menu_name = NULL);
+
+  /**
+   * Saves a plugin definition to the storage.
+   *
+   * @param array $definition
+   *   A definition for a \Drupal\Core\Menu\MenuLinkInterface plugin.
+   *
+   * @return array
+   *   The names of the menus affected by the save operation (1 or 2).
+   *
+   * @throws \Exception
+   *   If the storage back-end does not exist and could not be created.
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   If the definition is invalid - for example, if the specified parent
+   *   would cause the links children to be moved to greater than the maximum
+   *   depth.
+   */
+  public function save(array $definition);
+
+  /**
+   * Deletes a menu link definition from the storage.
+   *
+   * @param string $id
+   *   The menu link plugin ID.
+   */
+  public function delete($id);
+
+  /**
+   * Loads a menu link tree from the storage.
+   *
+   * 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.
+   * MenuLinkTree::checkAccess() needs to be invoked afterwards.
+   *
+   * The tree order is maintained using an optimized algorithm, for example by
+   * storing each parent in an individual field, see
+   * http://drupal.org/node/141866 for more details. However, any details
+   * of the storage should not be relied upon since it may be swapped with
+   * a different implementation.
+   *
+   * @param string $menu_name
+   *   The name of the menu.
+   * @param \Drupal\Core\Menu\MenuTreeParameters $parameters
+   *   The parameters to determine which menu links to be loaded into a tree.
+   *
+   * @return array
+   *   An array with 2 elements:
+   *   - tree: A fully built menu tree.
+   *   - route_names: An array of all route names used in the tree.
+   */
+  public function loadTreeData($menu_name, MenuTreeParameters $parameters);
+
+  /**
+   * Loads all the visible menu links that are below the given ID.
+   *
+   * The returned links are not ordered, and visible children will be
+   * included even if they have a hidden parent or ancestor so would not
+   * normally appear in a rendered tree.
+   *
+   * @param string $id
+   *   The parent menu link ID.
+   * @param int $max_relative_depth
+   *   The maximum relative depth of the children relative to the passed parent.
+   *
+   * @return array
+   *   An array of visible (not hidden) link definitions, keyed by ID.
+   */
+  public function loadAllChildren($id, $max_relative_depth = NULL);
+
+  /**
+   * Loads all the IDs for menu links that are below the given ID.
+   *
+   * @param string $id
+   *   The parent menu link ID.
+   *
+   * @return array
+   *   An unordered array of plugin IDs corresponding to all children.
+   */
+  public function getAllChildIds($id);
+
+  /**
+   * Loads a subtree rooted by the given ID.
+   *
+   * The returned links are structured like those from loadTreeData().
+   *
+   * @param string $id
+   *   The menu link plugin ID.
+   * @param int $max_relative_depth
+   *   The maximum depth of child menu links relative to the passed in.
+   *
+   * @return array
+   *   An array with 2 elements:
+   *   - subtree: A fully built menu tree element or FALSE.
+   *   - route_names: An array of all route names used in the subtree.
+   */
+  public function loadSubtreeData($id, $max_relative_depth = NULL);
+
+  /**
+   * Returns all the IDs that represent the path to the root of the tree.
+   *
+   * @param string $id
+   *   A menu link ID.
+   *
+   * @return array
+   *   An associative array of IDs with keys equal to values that represents the
+   *   path from the given ID  to the root of the tree. If $id is an ID that
+   *   exists, the returned array will at least include it.  An empty array
+   *   is returned if the ID does not exist in the storage.
+   */
+  public function getRootPathIds($id);
+
+  /**
+   * Find expanded links in a menu given a set of possible parents.
+   *
+   * @param string $menu_name
+   *   The menu name.
+   * @param array $parents
+   *   One or more parent IDs to match.
+   *
+   * @return array
+   *   The menu link IDs that are flagged as expanded in this menu.
+   */
+  public function getExpanded($menu_name, array $parents);
+
+  /**
+   * Finds the height of a subtree rooted by of the given ID.
+   *
+   * @param string $id
+   *   The the ID of an item in the storage.
+   *
+   * @return int
+   *   Returns the height of the subtree. This will be at least 1 if the ID
+   *   exists, or 0 if the ID does not exist in the storage.
+   */
+  public function getSubtreeHeight($id);
+
+  /**
+   * Determines whether a specific menu named is used in the tree.
+   *
+   * @param string $menu_name
+   *   The menu name.
+   *
+   * @return bool
+   *   Returns TRUE if the given menu name is used, otherwise FALSE.
+   */
+  public function menuNameInUse($menu_name);
+
+  /**
+   * Returns the used menu names in the tree storage.
+   *
+   * @return array
+   *   The menu names.
+   */
+  public function getMenuNames();
+
+  /**
+   * Counts the total number of menu links in one menu or all menus.
+   *
+   * @param string $menu_name
+   *   (optional) The menu name to count by, defaults to NULL.
+   *
+   * @return int
+   *   The number of menu links.
+   */
+  public function countMenuLinks($menu_name = NULL);
+
+}
diff --git a/core/lib/Drupal/Core/Menu/StaticMenuLinkOverrides.php b/core/lib/Drupal/Core/Menu/StaticMenuLinkOverrides.php
new file mode 100644
index 0000000..0c7a94b
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/StaticMenuLinkOverrides.php
@@ -0,0 +1,161 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\StaticMenuLinkOverrides.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+
+/**
+ * Implementation of the menu link override using a config file.
+ */
+class StaticMenuLinkOverrides implements StaticMenuLinkOverridesInterface {
+
+  /**
+   * The config name used to store the overrides.
+   *
+   * @var string
+   */
+  protected $configName = 'menu_link.static.overrides';
+
+  /**
+   * The menu link overrides config object.
+   *
+   * @var \Drupal\Core\Config\Config
+   */
+  protected $config;
+
+  /**
+   * The config factory object.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * Constructs a StaticMenuLinkOverrides object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   A configuration factory instance.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory) {
+    $this->configFactory = $config_factory;
+  }
+
+  /**
+   * Helper function to get the config object.
+   *
+   * Since this service is injected into all static menu link objects, but
+   * only used when updating one, avoid actually loading the config when it's
+   * not needed.
+   */
+  protected function getConfig() {
+    if (empty($this->config)) {
+      $this->config = $this->configFactory->get($this->configName);
+    }
+    return $this->config;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function reload() {
+    $this->config = NULL;
+    $this->configFactory->reset($this->configName);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadOverride($id) {
+    $all_overrides = $this->getConfig()->get('definitions');
+    $id = static::encodeId($id);
+    return $id && isset($all_overrides[$id]) ? $all_overrides[$id] : array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteMultipleOverrides(array $ids) {
+    $all_overrides = $this->getConfig()->get('definitions');
+    $save = FALSE;
+    foreach ($ids as $id) {
+      $id = static::encodeId($id);
+      if (isset($all_overrides[$id])) {
+        unset($all_overrides[$id]);
+        $save = TRUE;
+      }
+    }
+    if ($save) {
+      $this->getConfig()->set('definitions', $all_overrides)->save();
+    }
+    return $save;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteOverride($id) {
+    return $this->deleteMultipleOverrides(array($id));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadMultipleOverrides(array $ids) {
+    $result = array();
+    if ($ids) {
+      $all_overrides = $this->getConfig()->get('definitions') ?: array();
+      foreach ($ids as $id) {
+        $encoded_id = static::encodeId($id);
+        if (isset($all_overrides[$encoded_id])) {
+          $result[$id] = $all_overrides[$encoded_id];
+        }
+      }
+    }
+    return $result;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function saveOverride($id, array $definition) {
+    // Remove unexpected keys.
+    $expected = array(
+      'menu_name' => 1,
+      'parent' => 1,
+      'weight' => 1,
+      'expanded' => 1,
+      'hidden' => 1,
+    );
+    $definition = array_intersect_key($definition, $expected);
+    if ($definition) {
+      $id = static::encodeId($id);
+      $all_overrides = $this->getConfig()->get('definitions');
+      // Combine with any existing data.
+      $all_overrides[$id] = $definition + $this->loadOverride($id);
+      $this->getConfig()->set('definitions', $all_overrides)->save();
+    }
+    return array_keys($definition);
+  }
+
+  /**
+   * Encodes the ID by replacing dots with double underscores.
+   *
+   * This is done because config schema uses dots for its internal type
+   * hierarchy.
+   *
+   * @param string $id
+   *   The menu plugin ID.
+   *
+   * @return string
+   *   The menu plugin ID with double underscore instead of dots.
+   */
+  protected static function encodeId($id) {
+    return str_replace('.', '__', $id);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php b/core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php
new file mode 100644
index 0000000..5f0549b
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\StaticMenuLinkOverridesInterface.
+ */
+
+namespace Drupal\Core\Menu;
+
+/**
+ * Defines an interface for objects which overrides menu links defined in YAML.
+ */
+interface StaticMenuLinkOverridesInterface {
+
+  /**
+   * Force all overrides to be re-loaded from storage. Useful for testing.
+   */
+  public function reload();
+
+  /**
+   * Loads any overrides to the definition of a static (YAML-defined) link.
+   *
+   * @param string $id
+   *   A menu link plugin ID.
+   *
+   * @return array|NULL
+   *   An override with following supported keys:
+   *     - parent
+   *     - weight
+   *     - menu_name
+   *     - expanded
+   *     - hidden
+   */
+  public function loadOverride($id);
+
+  /**
+   * Deletes any overrides to the definition of a static (YAML-defined) link.
+   *
+   * @param string $id
+   *   A menu link plugin ID.
+   */
+  public function deleteOverride($id);
+
+  /**
+   * Deletes multiple overrides to definitions of static (YAML-defined) links.
+   *
+   * @param array $ids
+   *   Array of menu link plugin IDs.
+   */
+  public function deleteMultipleOverrides(array $ids);
+
+  /**
+   * Loads overrides to multiple definitions of a static (YAML-defined) link.
+   *
+   * @param array $ids
+   *   Array of menu link plugin IDs.
+   *
+   * @return array
+   *   One or override keys by plugin ID.
+   *
+   * @see \Drupal\Core\Menu\StaticMenuLinkOverridesInterface
+   */
+  public function loadMultipleOverrides(array $ids);
+
+  /**
+   * Saves the override.
+   *
+   * @param string $id
+   *   A menu link plugin ID.
+   * @param array $definition
+   *   The definition values to override. Supported keys:
+   *   - menu_name
+   *   - parent
+   *   - weight
+   *   - expanded
+   *   - hidden
+   *
+   * @return array
+   *   A list of properties which got saved.
+   */
+  public function saveOverride($id, array $definition);
+
+}
diff --git a/core/lib/Drupal/Core/Plugin/CachedDiscoveryClearer.php b/core/lib/Drupal/Core/Plugin/CachedDiscoveryClearer.php
index 5febcd3..9ab429f 100644
--- a/core/lib/Drupal/Core/Plugin/CachedDiscoveryClearer.php
+++ b/core/lib/Drupal/Core/Plugin/CachedDiscoveryClearer.php
@@ -21,7 +21,7 @@ class CachedDiscoveryClearer {
    *
    * @var \Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface[]
    */
-  protected $cachedDiscoveries;
+  protected $cachedDiscoveries = array();
 
   /**
    * Adds a plugin manager to the active list.
diff --git a/core/lib/Drupal/Core/Plugin/PluginManagerPass.php b/core/lib/Drupal/Core/Plugin/PluginManagerPass.php
index 8bfde35..9efdcf2 100644
--- a/core/lib/Drupal/Core/Plugin/PluginManagerPass.php
+++ b/core/lib/Drupal/Core/Plugin/PluginManagerPass.php
@@ -23,7 +23,9 @@ public function process(ContainerBuilder $container) {
     $cache_clearer_definition = $container->getDefinition('plugin.cache_clearer');
     foreach ($container->getDefinitions() as $service_id => $definition) {
       if (strpos($service_id, 'plugin.manager.') === 0 || $definition->hasTag('plugin_manager_cache_clear')) {
-        $cache_clearer_definition->addMethodCall('addCachedDiscovery', array(new Reference($service_id)));
+        if (is_subclass_of($definition->getClass(), '\Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface')) {
+          $cache_clearer_definition->addMethodCall('addCachedDiscovery', array(new Reference($service_id)));
+        }
       }
     }
   }
diff --git a/core/modules/menu_link_content/menu_link_content.info.yml b/core/modules/menu_link_content/menu_link_content.info.yml
new file mode 100644
index 0000000..a82c890
--- /dev/null
+++ b/core/modules/menu_link_content/menu_link_content.info.yml
@@ -0,0 +1,6 @@
+name: 'Menu Link Content'
+type: module
+description: 'Allows administrators to create custom links'
+package: Core
+version: VERSION
+core: 8.x
diff --git a/core/modules/menu_link_content/menu_link_content.module b/core/modules/menu_link_content/menu_link_content.module
new file mode 100644
index 0000000..6648280
--- /dev/null
+++ b/core/modules/menu_link_content/menu_link_content.module
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @file
+ * Enables users to create menu link content.
+ */
+
+use Drupal\system\MenuInterface;
+
+/**
+ * Implements hook_menu_delete().
+ */
+function menu_link_content_menu_delete(MenuInterface $menu) {
+  $storage = \Drupal::entityManager()->getStorage('menu_link_content');
+  $menu_links = $storage->loadByProperties(array('menu_name' => $menu->id()));
+  $storage->delete($menu_links);
+}
diff --git a/core/modules/menu_link_content/src/Entity/MenuLinkContent.php b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
new file mode 100644
index 0000000..9ac769f
--- /dev/null
+++ b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
@@ -0,0 +1,387 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_link_content\Entity\MenuLinkContent.
+ */
+
+namespace Drupal\menu_link_content\Entity;
+
+use Drupal\Core\Entity\ContentEntityBase;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\FieldDefinition;
+use Drupal\Core\Url;
+
+/**
+ * Defines the menu link content entity class.
+ *
+ * @ContentEntityType(
+ *   id = "menu_link_content",
+ *   label = @Translation("Menu link content"),
+ *   controllers = {
+ *     "storage" = "Drupal\Core\Entity\ContentEntityDatabaseStorage",
+ *     "access" = "Drupal\menu_link_content\MenuLinkContentAccessController",
+ *     "form" = {
+ *       "default" = "Drupal\menu_link_content\Form\MenuLinkContentForm",
+ *       "delete" = "Drupal\menu_link_content\Form\MenuLinkContentDeleteForm"
+ *     }
+ *   },
+ *   admin_permission = "administer menu",
+ *   base_table = "menu_link_content",
+ *   data_table = "menu_link_content_data",
+ *   fieldable = TRUE,
+ *   translatable = TRUE,
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "title",
+ *     "uuid" = "uuid",
+ *     "bundle" = "bundle"
+ *   },
+ *   links = {
+ *     "canonical" = "menu_link_content.link_edit",
+ *     "edit-form" = "menu_link_content.link_edit",
+ *   }
+ * )
+ */
+class MenuLinkContent extends ContentEntityBase implements MenuLinkContentInterface {
+
+  /**
+   * A flag for whether this entity is wrapped in a plugin instance.
+   *
+   * @var bool
+   */
+  protected $insidePlugin = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setInsidePlugin() {
+    $this->insidePlugin = TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTitle() {
+    return $this->get('title')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRouteName() {
+    return $this->get('route_name')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRouteParameters() {
+    return $this->get('route_parameters')->first()->getValue();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setRouteParameters(array $route_parameters) {
+    $this->set('route_parameters', array($route_parameters));
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getUrl() {
+    return $this->get('url')->value ?: NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getUrlObject() {
+    if ($route_name = $this->getRouteName()) {
+      $url = new Url($route_name, $this->getRouteParameters(), $this->getOptions());
+    }
+    else {
+      $path = $this->getUrl();
+      if (isset($path)) {
+        $url = Url::createFromPath($path);
+      }
+      else {
+        $url = new Url('<front>');
+      }
+    }
+
+    return $url;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMenuName() {
+    return $this->get('menu_name')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOptions() {
+    return $this->get('options')->first()->getValue();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOptions(array $options) {
+    $this->set('options', array($options));
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return $this->get('description')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPluginId() {
+    return 'menu_link_content:' . $this->uuid();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isHidden() {
+    return (bool) $this->get('hidden')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isExpanded() {
+    return (bool) $this->get('expanded')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getParentId() {
+    return $this->get('parent')->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getWeight() {
+    return (int) $this->get('weight')->value;
+  }
+
+  /**
+   * Builds up the menu link plugin definition for this entity.
+   *
+   * @return array
+   *   The plugin definition corresponding to this entity.
+   *
+   * @see \Drupal\Core\Menu\MenuLinkTree::$defaults
+   */
+  protected function getMenuDefinition() {
+    $definition = array();
+    $definition['class'] = 'Drupal\menu_link_content\Plugin\Menu\MenuLinkContent';
+    $definition['menu_name'] = $this->getMenuName();
+    $definition['route_name'] = $this->getRouteName();
+    $definition['route_parameters'] = $this->getRouteParameters();
+    $definition['url'] = $this->getUrl();
+    $definition['options'] = $this->getOptions();
+    // Don't bother saving title and description strings, since they are never
+    // used.
+    $definition['title'] = $this->getTitle();
+    $definition['description'] = $this->getDescription();
+    $definition['weight'] = $this->getWeight();
+    $definition['id'] = $this->getPluginId();
+    $definition['metadata'] = array('entity_id' => $this->id());
+    $definition['form_class'] = '\Drupal\menu_link_content\Form\MenuLinkContentForm';
+    $definition['hidden'] = $this->isHidden() ? 1 : 0;
+    $definition['expanded'] = $this->isExpanded() ? 1 : 0;
+    $definition['provider'] = 'menu_link_content';
+    $definition['discovered'] = 0;
+    $definition['parent'] = $this->getParentId();
+
+    return $definition;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
+    parent::postSave($storage, $update);
+
+    /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
+    $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
+
+    // The menu link can just be updated if there is already an menu link entry
+    // on both entity and menu link plugin level.
+    if ($update && $menu_link_manager->getDefinition($this->getPluginId())) {
+      // When the entity is saved via a plugin instance, we should not call
+      // the menu tree manager to update the definition a second time.
+      if (!$this->insidePlugin) {
+        $menu_link_manager->updateLink($this->getPluginId(), $this->getMenuDefinition(), FALSE);
+      }
+    }
+    else {
+      $menu_link_manager->createLink($this->getPluginId(), $this->getMenuDefinition());
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function preDelete(EntityStorageInterface $storage, array $entities) {
+    parent::preDelete($storage, $entities);
+
+    /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
+    $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
+
+    foreach ($entities as $menu_link) {
+      /** @var \Drupal\menu_link_content\Entity\MenuLinkContent $menu_link */
+      $menu_link_manager->deleteLink($menu_link->getPluginId(), FALSE);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields['id'] = FieldDefinition::create('integer')
+      ->setLabel(t('Content menu link ID'))
+      ->setDescription(t('The menu link ID.'))
+      ->setReadOnly(TRUE)
+      ->setSetting('unsigned', TRUE);
+
+    $fields['uuid'] = FieldDefinition::create('uuid')
+      ->setLabel(t('UUID'))
+      ->setDescription(t('The content menu link UUID.'))
+      ->setReadOnly(TRUE);
+
+    $fields['bundle'] = FieldDefinition::create('string')
+      ->setLabel(t('Bundle'))
+      ->setDescription(t('The content menu link bundle.'))
+      ->setSetting('max_length', EntityTypeInterface::BUNDLE_MAX_LENGTH)
+      ->setReadOnly(TRUE);
+
+    $fields['title'] = FieldDefinition::create('string')
+      ->setLabel(t('Menu link title'))
+      ->setDescription(t('The text to be used for this link in the menu.'))
+      ->setRequired(TRUE)
+      ->setTranslatable(TRUE)
+      ->setSettings(array(
+        'default_value' => '',
+        'max_length' => 255,
+      ))
+      ->setDisplayOptions('view', array(
+        'label' => 'hidden',
+        'type' => 'string',
+        'weight' => -5,
+      ))
+      ->setDisplayOptions('form', array(
+        'type' => 'string',
+        'weight' => -5,
+      ))
+      ->setDisplayConfigurable('form', TRUE);
+
+    $fields['description'] = FieldDefinition::create('string')
+      ->setLabel(t('Description'))
+      ->setDescription(t('Shown when hovering over the menu link.'))
+      ->setTranslatable(TRUE)
+      ->setSettings(array(
+        'default_value' => '',
+        'max_length' => 255,
+      ))
+      ->setDisplayOptions('view', array(
+        'label' => 'hidden',
+        'type' => 'string',
+        'weight' => 0,
+      ))
+      ->setDisplayOptions('form', array(
+        'type' => 'string',
+        'weight' => 0,
+      ));
+
+    $fields['menu_name'] = FieldDefinition::create('string')
+      ->setLabel(t('Menu name'))
+      ->setDescription(t('The menu name. All links with the same menu name (such as "tools") are part of the same menu.'))
+      ->setSetting('default_value', 'tools');
+
+    // @todo use a link field in the end? see https://drupal.org/node/2235457
+    $fields['route_name'] = FieldDefinition::create('string')
+      ->setLabel(t('Route name'))
+      ->setDescription(t('The machine name of a defined Symfony Route this menu item represents.'));
+
+    $fields['route_parameters'] = FieldDefinition::create('map')
+      ->setLabel(t('Route parameters'))
+      ->setDescription(t('A serialized array of route parameters of this menu link.'));
+
+    $fields['url'] = FieldDefinition::create('string')
+      ->setLabel(t('External link url'))
+      ->setDescription(t('The url of the link, in case you have an external link.'));
+
+    $fields['options'] = FieldDefinition::create('map')
+      ->setLabel(t('Options'))
+      ->setDescription(t('A serialized array of options to be passed to the url() or l() function, such as a query string or HTML attributes.'))
+      ->setSetting('default_value', array());
+
+    $fields['external'] = FieldDefinition::create('boolean')
+      ->setLabel(t('External'))
+      ->setDescription(t('A flag to indicate if the link points to a full URL starting with a protocol, like http:// (1 = external, 0 = internal).'))
+      ->setSetting('default_value', 0);
+
+    // The form widget doesn't work yet for core fields, so we skip the
+    // for display and manually create form elements for the boolean fields.
+    // @see https://drupal.org/node/2226493
+    // @see https://drupal.org/node/2150511
+    $fields['expanded'] = FieldDefinition::create('boolean')
+      ->setLabel(t('Expanded'))
+      ->setDescription(t('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).'))
+      ->setSetting('default_value', 0)
+      ->setDisplayOptions('view', array(
+        'label' => 'hidden',
+        'type' => 'boolean',
+        'weight' => 0,
+      ));
+
+    // We manually create a form element for this, since the form logic is
+    // is inverted to show enabled.
+    $fields['hidden'] = FieldDefinition::create('boolean')
+      ->setLabel(t('Hidden'))
+      ->setDescription(t('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).'))
+      ->setSetting('default_value', 0);
+
+    $fields['weight'] = FieldDefinition::create('integer')
+      ->setLabel(t('Weight'))
+      ->setDescription(t('Link weight among links in the same menu at the same depth.'))
+      ->setSetting('default_value', 0)
+      ->setDisplayOptions('view', array(
+        'label' => 'hidden',
+        'type' => 'integer',
+        'weight' => 0,
+      ))
+      ->setDisplayOptions('form', array(
+        'type' => 'integer',
+        'weight' => 0,
+      ));
+
+    $fields['langcode'] = FieldDefinition::create('language')
+      ->setLabel(t('Language code'))
+      ->setDescription(t('The node language code.'));
+
+    $fields['parent'] = FieldDefinition::create('string')
+      ->setLabel(t('Parent menu link ID'))
+      ->setDescription(t('The parent menu link ID.'));
+
+    return $fields;
+  }
+
+}
diff --git a/core/modules/menu_link_content/src/Entity/MenuLinkContentInterface.php b/core/modules/menu_link_content/src/Entity/MenuLinkContentInterface.php
new file mode 100644
index 0000000..35a24a7
--- /dev/null
+++ b/core/modules/menu_link_content/src/Entity/MenuLinkContentInterface.php
@@ -0,0 +1,147 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_link_content\Entity\MenuLinkContentInterface.
+ */
+
+namespace Drupal\menu_link_content\Entity;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+
+/**
+ * Defines an interface for custom menu links.
+ */
+interface MenuLinkContentInterface extends ContentEntityInterface {
+
+  /**
+   * Flag this instance as being wrapped in a menu link plugin instance.
+   */
+  public function setInsidePlugin();
+
+  /**
+   * Gets the title of the menu link.
+   *
+   * @return string
+   *   The title of the link.
+   */
+  public function getTitle();
+
+  /**
+   * Gets the route name of the custom menu link.
+   *
+   * @return string|NULL
+   *   Returns the route name, unless it is an internal link.
+   */
+  public function getRouteName();
+
+  /**
+   * Gets the route parameters of the custom menu link.
+   *
+   * @return array
+   *   The route parameters, or an empty array.
+   */
+  public function getRouteParameters();
+
+  /**
+   * Sets the route paramters of the custom menu link.
+   *
+   * @param array $route_parameters
+   *   The route parameters
+   *
+   * @return $this
+   */
+  public function setRouteParameters(array $route_parameters);
+
+  /**
+   * Gets the external URL.
+   *
+   * @return string|NULL
+   *   Returns the external URL if the menu link points to an external URL,
+   *   otherwise NULL.
+   */
+  public function getUrl();
+
+  /**
+   * Gets the url object pointing to the URL of the custom menu link.
+   *
+   * @return \Drupal\Core\Url
+   *   A Url object instance.
+   */
+  public function getUrlObject();
+
+  /**
+   * Gets the menu name of the custom menu link.
+   *
+   * @return string
+   *   The menu ID.
+   */
+  public function getMenuName();
+
+  /**
+   * Gets the options for the custom menu link.
+   *
+   * @return array
+   *   The options that may be passed to the URL generator.
+   */
+  public function getOptions();
+
+  /**
+   * Sets the query options of the custom menu link.
+   *
+   * @param array $options
+   *   The new option.
+   *
+   * @return $this
+   */
+  public function setOptions(array $options);
+
+  /**
+   * Gets the description of the custom menu link for the UI.
+   *
+   * @return string
+   *   The descption for use on admin pages or as a title attribute.
+   */
+  public function getDescription();
+
+  /**
+   * Gets the menu plugin ID associated with this entity.
+   *
+   * @return string
+   *   The plugin ID.
+   */
+  public function getPluginId();
+
+  /**
+   * Returns whether the menu link is marked as hidden.
+   *
+   * @return bool
+   *   TRUE if is not enabled, otherwise FALSE.
+   */
+  public function isHidden();
+
+  /**
+   * Returns whether the menu link is marked as always expanded.
+   *
+   * @return bool
+   *   TRUE for expanded, FALSE otherwise.
+   */
+  public function isExpanded();
+
+  /**
+   * Gets the plugin ID of the parent menu link.
+   *
+   * @return string
+   *   A plugin ID, or empty string if this link is at the top level.
+   */
+  public function getParentId();
+
+  /**
+   * Returns the weight of the custom menu link.
+   *
+   * @return int
+   *   A weight for use when ordering links.
+   */
+  public function getWeight();
+
+}
diff --git a/core/modules/menu_link_content/src/MenuLinkContentAccessController.php b/core/modules/menu_link_content/src/MenuLinkContentAccessController.php
new file mode 100644
index 0000000..d895478
--- /dev/null
+++ b/core/modules/menu_link_content/src/MenuLinkContentAccessController.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\menu_link_content\MenuLinkContentAccessController.
+ */
+
+namespace Drupal\menu_link_content;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityAccessController;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Defines the access controller for the user entity type.
+ */
+class MenuLinkContentAccessController extends EntityAccessController {
+
+  /**
+   * The access manager to check routes by name.
+   *
+   * @var \Drupal\Core\Access\AccessManager
+   */
+  protected $accessManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
+    switch ($operation) {
+      case 'view':
+        // There is no direct view.
+        return FALSE;
+
+      case 'update':
+        // If there is a URL, this is an external link so always accessible.
+        return $account->hasPermission('administer menu') && ($entity->getUrl() || $this->accessManager()->checkNamedRoute($entity->getRouteName(), $entity->getRouteParameters(), $account));
+
+      case 'delete':
+        return !$entity->isNew() && $account->hasPermission('administer menu');
+    }
+  }
+
+  /**
+   * Returns the access manager.
+   *
+   * @return \Drupal\Core\Access\AccessManager
+   *   The route provider.
+   */
+  protected function accessManager() {
+    if (!$this->accessManager) {
+      $this->accessManager = \Drupal::service('access_manager');
+    }
+    return $this->accessManager;
+  }
+}
diff --git a/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php b/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php
new file mode 100644
index 0000000..9c77e53
--- /dev/null
+++ b/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php
@@ -0,0 +1,234 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent.
+ */
+
+namespace Drupal\menu_link_content\Plugin\Menu;
+
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Menu\MenuLinkBase;
+use Drupal\Component\Plugin\Exception\PluginException;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides the menu link plugin for content menu link.s
+ */
+class MenuLinkContent extends MenuLinkBase implements ContainerFactoryPluginInterface {
+
+  protected static $entityIdsToLoad = array();
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $overrideAllowed = array(
+    'menu_name' => 1,
+    'parent' => 1,
+    'weight' => 1,
+    'expanded' => 1,
+    'hidden' => 1,
+    'title' => 1,
+    'description' => 1,
+    'route_name' => 1,
+    'route_parameters' => 1,
+    'url' => 1,
+    'options' => 1,
+  );
+
+  /**
+   * The menu link content entity connected to this plugin instance.
+   *
+   * @var \Drupal\menu_link_content\Entity\MenuLinkContentInterface
+   */
+  protected $entity;
+
+  /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface
+   */
+  protected $entityManager;
+
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface
+   */
+  protected $languageManager;
+
+  /**
+   * Constructs a new MenuLinkContent.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   The entity manager.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    if (!empty($this->pluginDefinition['metadata']['entity_id'])) {
+      $entity_id = $this->pluginDefinition['metadata']['entity_id'];
+      static::$entityIdsToLoad[$entity_id] = $entity_id;
+    }
+
+    $this->entityManager = $entity_manager;
+    $this->langaugeManager = $language_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity.manager'),
+      $container->get('language_manager')
+    );
+  }
+
+  /**
+   * Loads the entity associated with this menu link.
+   *
+   * @return \Drupal\menu_link_content\Entity\MenuLinkContentInterface
+   *   The menu link content entity.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   If the entity ID and uuid are both invalid or missing.
+   */
+  protected function getEntity() {
+    if (empty($this->entity)) {
+      $entity = NULL;
+      $storage = $this->entityManager->getStorage('menu_link_content');
+      if (!empty($this->pluginDefinition['metadata']['entity_id'])) {
+        $entity_id = $this->pluginDefinition['metadata']['entity_id'];
+        static::$entityIdsToLoad[$entity_id] = $entity_id;
+        $entities = $storage->loadMultiple(array_values(static::$entityIdsToLoad));
+        $entity = isset($entities[$entity_id]) ? $entities[$entity_id] : NULL;
+        static::$entityIdsToLoad = array();
+      }
+      else {
+        // Fallback to the loading by the uuid.
+        $uuid = $this->getDerivativeId();
+        $links = $storage->loadByProperties(array('uuid' => $uuid));
+        $entity = reset($links);
+      }
+      if (!$entity) {
+        throw new PluginException("Invalid entity ID or uuid");
+      }
+      // Clone the entity object to avoid tampering with the static cache.
+      $this->entity = clone $entity;
+      $this->entity = $this->entityManager->getTranslationFromContext($this->entity);
+      $this->entity->setInsidePlugin();
+    }
+    return $this->entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTitle() {
+    // We only need to get the title from the actual entity if it may be
+    // a translation based on the current language context.  This can only
+    // happen if the site configured to be multilingual.
+    if ($this->langaugeManager->isMultilingual()) {
+      return $this->getEntity()->getTitle();
+    }
+    return $this->pluginDefinition['title'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    if ($this->langaugeManager->isMultilingual()) {
+      return $this->getEntity()->getDescription();
+    }
+    return $this->pluginDefinition['description'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDeleteRoute() {
+    return array(
+      'route_name' => 'menu_link_content.link_delete',
+      'route_parameters' => array('menu_link_content' => $this->getEntity()->id()),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEditRoute() {
+    return array(
+      'route_name' => 'menu_link_content.link_edit',
+      'route_parameters' => array('menu_link_content' => $this->getEntity()->id()),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTranslateRoute() {
+    $entity_type = $this->getEntity()->getEntityType()->id();
+    return array(
+      'route_name' => 'content_translation.translation_overview_' . $entity_type,
+      'route_parameters' => array(
+        $entity_type => $this->getEntity()->id(),
+      ),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateLink(array $new_definition_values, $persist) {
+    $overrides = array_intersect_key($new_definition_values, $this->overrideAllowed);
+    // Update the definition.
+    $this->pluginDefinition = $overrides + $this->getPluginDefinition();
+    if ($persist) {
+      $entity = $this->getEntity();
+      foreach ($overrides as $key => $value) {
+        $entity->{$key}->value = $value;
+      }
+      $this->entityManager->getStorage('menu_link_content')->save($entity);
+    }
+
+    return $this->pluginDefinition;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isDeletable() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isTranslatable() {
+    return $this->getEntity()->isTranslatable();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteLink() {
+    // @todo: Flag this call if possible so we don't call the menu tree manager.
+    $this->getEntity()->delete();
+  }
+
+}
diff --git a/core/modules/system/src/Tests/Menu/MenuLinkTreeTest.php b/core/modules/system/src/Tests/Menu/MenuLinkTreeTest.php
new file mode 100644
index 0000000..62654db
--- /dev/null
+++ b/core/modules/system/src/Tests/Menu/MenuLinkTreeTest.php
@@ -0,0 +1,151 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Menu\MenuLinkTreeTest.
+ */
+
+namespace Drupal\system\Tests\Menu;
+
+use Drupal\Core\Menu\MenuLinkTreeElement;
+use Drupal\Core\Menu\MenuTreeParameters;
+use Drupal\simpletest\KernelTestBase;
+use Drupal\Tests\Core\Menu\MenuLinkMock;
+
+/**
+ * Tests the menu link tree.
+ *
+ * @see \Drupal\Core\Menu\MenuLinkTree
+ */
+class MenuLinkTreeTest extends KernelTestBase {
+
+  /**
+   * The tested menu link tree.
+   *
+   * @var \Drupal\Core\Menu\MenuLinkTree
+   */
+  protected $linkTree;
+
+  /**
+   * The menu link plugin mananger
+   *
+   * @var \Drupal\Core\Menu\MenuLinkManagerInterface $menuLinkManager
+   */
+  protected $menuLinkManager;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array(
+    'system',
+    'menu_test',
+    'menu_link',
+    'menu_link_content',
+    'field',
+  );
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Tests \Drupal\Core\Menu\MenuLinkTree',
+      'description' => '',
+      'group' => 'Menu',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installSchema('system', array('router'));
+    $this->installEntitySchema('menu_link_content');
+
+    $this->linkTree = $this->container->get('menu.link_tree');
+    $this->menuLinkManager = $this->container->get('plugin.manager.menu.link');
+  }
+
+  /**
+   * Tests deleting all the links in a menu.
+   */
+  public function testDeleteLinksInMenu() {
+    \Drupal::service('router.builder')->rebuild();
+
+    \Drupal::entityManager()->getStorage('menu')->create(array('id' => 'menu1'))->save();
+    \Drupal::entityManager()->getStorage('menu')->create(array('id' => 'menu2'))->save();
+
+    \Drupal::entityManager()->getStorage('menu_link_content')->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content'))->save();
+    \Drupal::entityManager()->getStorage('menu_link_content')->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content'))->save();
+    \Drupal::entityManager()->getStorage('menu_link_content')->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu2', 'bundle' => 'menu_link_content'))->save();
+
+    $output = $this->linkTree->load('menu1', new MenuTreeParameters());
+    $this->assertEqual(count($output), 2);
+    $output = $this->linkTree->load('menu2', new MenuTreeParameters());
+    $this->assertEqual(count($output), 1);
+
+    $this->menuLinkManager->deleteLinksInMenu('menu1');
+
+    $output = $this->linkTree->load('menu1', new MenuTreeParameters());
+    $this->assertEqual(count($output), 0);
+
+    $output = $this->linkTree->load('menu2', new MenuTreeParameters());
+    $this->assertEqual(count($output), 1);
+  }
+
+  /**
+   * Tests creating links with an expected tree structure.
+   */
+  public function testCreateLinksInMenu() {
+    /**
+     * This creates a tree with the following structure:
+     * - 1
+     * - 2
+     *   - 3
+     *     - 4
+     * - 5
+     *   - 7
+     * - 6
+     * - 8
+     *
+     * With link 6 being the only external link.
+     */
+    $links = array(
+      1 => MenuLinkMock::create(array('id' => 'test.example1', 'route_name' => 'example1', 'title' => 'foo', 'parent' => '')),
+      2 => MenuLinkMock::create(array('id' => 'test.example2', 'route_name' => 'example2', 'title' => 'bar', 'parent' => 'test.example1', 'route_parameters' => array('foo' => 'bar'))),
+      3 => MenuLinkMock::create(array('id' => 'test.example3', 'route_name' => 'example3', 'title' => 'baz', 'parent' => 'test.example2', 'route_parameters' => array('baz' => 'qux'))),
+      4 => MenuLinkMock::create(array('id' => 'test.example4', 'route_name' => 'example4', 'title' => 'qux', 'parent' => 'test.example3')),
+      5 => MenuLinkMock::create(array('id' => 'test.example5', 'route_name' => 'example5', 'title' => 'foofoo', 'parent' => '')),
+      6 => MenuLinkMock::create(array('id' => 'test.example6', 'route_name' => '', 'url' => 'https://drupal.org/', 'title' => 'barbar', 'parent' => '')),
+      7 => MenuLinkMock::create(array('id' => 'test.example7', 'route_name' => 'example7', 'title' => 'bazbaz', 'parent' => '')),
+      8 => MenuLinkMock::create(array('id' => 'test.example8', 'route_name' => 'example8', 'title' => 'quxqux', 'parent' => '')),
+    );
+    foreach ($links as $instance) {
+      $this->menuLinkManager->createLink($instance->getPluginId(), $instance->getPluginDefinition());
+    }
+    $parameters = new MenuTreeParameters();
+    $tree = $this->linkTree->load('mock', $parameters);
+
+    $count = function(array $tree) {
+      $sum = function ($carry, MenuLinkTreeElement $item) {
+        return $carry + $item->count();
+      };
+      return array_reduce($tree, $sum);
+    };
+
+    $this->assertEqual($count($tree), 8);
+    $parameters = new MenuTreeParameters();
+    $parameters->setRoot('test.example2');
+    $tree = $this->linkTree->load($instance->getMenuName(), $parameters);
+    $top_link = reset($tree);
+    $this->assertEqual(count($top_link->subtree), 1);
+    $child = reset($top_link->subtree);
+    $this->assertEqual($child->link->getPluginId(), $links[3]->getPluginId());
+    $height = $this->linkTree->getSubtreeHeight('test.example2');
+    $this->assertEqual($height, 3);
+  }
+
+}
diff --git a/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php b/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php
new file mode 100644
index 0000000..8064984
--- /dev/null
+++ b/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php
@@ -0,0 +1,398 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Menu\MenuTreeStorageTest.
+ */
+
+namespace Drupal\system\Tests\Menu;
+
+use Drupal\Component\Plugin\Exception\PluginException;
+use Drupal\Core\Menu\MenuTreeParameters;
+use Drupal\Core\Menu\MenuTreeStorage;
+use Drupal\simpletest\KernelTestBase;
+
+/**
+ * Tests the menu tree storage.
+ *
+ * @see \Drupal\Core\Menu\MenuTreeStorage
+ */
+class MenuTreeStorageTest extends KernelTestBase {
+
+  /**
+   * The tested tree storage.
+   *
+   * @var \Drupal\Core\Menu\MenuTreeStorage
+   */
+  protected $treeStorage;
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('system', 'menu_link_content');
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Menu tree storage tests',
+      'description' => 'Tests menu tree storage tests',
+      'group' => 'Menu',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->treeStorage = new MenuTreeStorage($this->container->get('database'), $this->container->get('cache.menu'), 'menu_tree');
+    $this->connection = $this->container->get('database');
+    $this->installEntitySchema('menu_link_content');
+  }
+
+  /**
+   * Tests the tree storage when no tree was built yet.
+   */
+  public function testBasicMethods() {
+    $this->doTestEmptyStorage();
+    $this->doTestTable();
+  }
+
+  /**
+   * Ensures that there are no menu links by default.
+   */
+  protected function doTestEmptyStorage() {
+    $this->assertEqual(0, $this->treeStorage->countMenuLinks());
+  }
+
+  /**
+   * Ensures that table gets created on the fly.
+   */
+  protected function doTestTable() {
+    // Test that we can create a tree storage with an arbitrary table name and
+    // that selecting from the storage creates the table.
+    $tree_storage = new MenuTreeStorage($this->container->get('database'), $this->container->get('cache.menu'), 'test_menu_tree');
+    $this->assertFalse($this->connection->schema()->tableExists('test_menu_tree'), 'Test table is not yet created');
+    $tree_storage->countMenuLinks();
+    $this->assertTrue($this->connection->schema()->tableExists('test_menu_tree'), 'Test table was created');
+  }
+
+  /**
+   * Tests with a simple linear hierarchy.
+   */
+  public function testSimpleHierarchy() {
+    // Add some links with parent on the previous one and test some values.
+    // <tools>
+    // - test1
+    // -- test2
+    // --- test3
+    $this->addMenuLink('test1', '');
+    $this->assertMenuLink('test1', array('has_children' => 0, 'depth' => 1));
+
+    $this->addMenuLink('test2', 'test1');
+    $this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test2'));
+    $this->assertMenuLink('test2', array('has_children' => 0, 'depth' => 2), array('test1'));
+
+    $this->addMenuLink('test3', 'test2');
+    $this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test2', 'test3'));
+    $this->assertMenuLink('test2', array('has_children' => 1, 'depth' => 2), array('test1'), array('test3'));
+    $this->assertMenuLink('test3', array('has_children' => 0, 'depth' => 3), array('test2', 'test1'));
+  }
+
+  /**
+   * Tests the tree with moving links inside the hierarchy.
+   */
+  public function testMenuLinkMoving() {
+    // Before the move.
+    // <tools>
+    // - test1
+    // -- test2
+    // --- test3
+    // - test4
+    // -- test5
+    // --- test6
+
+    $this->addMenuLink('test1', '');
+    $this->addMenuLink('test2', 'test1');
+    $this->addMenuLink('test3', 'test2');
+    $this->addMenuLink('test4', '');
+    $this->addMenuLink('test5', 'test4');
+    $this->addMenuLink('test6', 'test5');
+
+    $this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test2', 'test3'));
+    $this->assertMenuLink('test2', array('has_children' => 1, 'depth' => 2), array('test1'), array('test3'));
+    $this->assertMenuLink('test4', array('has_children' => 1, 'depth' => 1), array(), array('test5', 'test6'));
+    $this->assertMenuLink('test5', array('has_children' => 1, 'depth' => 2), array('test4'), array('test6'));
+    $this->assertMenuLink('test6', array('has_children' => 0, 'depth' => 3), array('test5', 'test4'));
+
+    $this->moveMenuLink('test2', 'test5');
+    // After the 1st move.
+    // <tools>
+    // - test1
+    // - test4
+    // -- test5
+    // --- test2
+    // ---- test3
+    // --- test6
+
+    $this->assertMenuLink('test1', array('has_children' => 0, 'depth' => 1));
+    $this->assertMenuLink('test2', array('has_children' => 1, 'depth' => 3), array('test5', 'test4'), array('test3'));
+    $this->assertMenuLink('test3', array('has_children' => 0, 'depth' => 4), array('test2', 'test5', 'test4'));
+    $this->assertMenuLink('test4', array('has_children' => 1, 'depth' => 1), array(), array('test5', 'test2', 'test3', 'test6'));
+    $this->assertMenuLink('test5', array('has_children' => 1, 'depth' => 2), array('test4'), array('test2', 'test3', 'test6'));
+    $this->assertMenuLink('test6', array('has_children' => 0, 'depth' => 3), array('test5', 'test4'));
+
+    $this->moveMenuLink('test4', 'test1');
+    $this->moveMenuLink('test3', 'test1');
+    // After the next 2 moves.
+    // <tools>
+    // - test1
+    // -- test3
+    // -- test4
+    // --- test5
+    // ---- test2
+    // ---- test6
+
+    $this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test4', 'test5', 'test2', 'test3', 'test6'));
+    $this->assertMenuLink('test2', array('has_children' => 0, 'depth' => 4), array('test5', 'test4', 'test1'));
+    $this->assertMenuLink('test3', array('has_children' => 0, 'depth' => 2), array('test1'));
+    $this->assertMenuLink('test4', array('has_children' => 1, 'depth' => 2), array('test1'), array('test2', 'test5', 'test6'));
+    $this->assertMenuLink('test5', array('has_children' => 1, 'depth' => 3), array('test4', 'test1'), array('test2', 'test6'));
+    $this->assertMenuLink('test6', array('has_children' => 0, 'depth' => 4), array('test5', 'test4', 'test1'));
+
+    // Deleting a link in the middle should re-attach child links to the parent.
+    $this->treeStorage->delete('test4');
+    // After the delete.
+    // <tools>
+    // - test1
+    // -- test3
+    // -- test5
+    // --- test2
+    // --- test6
+    $this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test5', 'test2', 'test3', 'test6'));
+    $this->assertMenuLink('test2', array('has_children' => 0, 'depth' => 3), array('test5', 'test1'));
+    $this->assertMenuLink('test3', array('has_children' => 0, 'depth' => 2), array('test1'));
+    $this->assertFalse($this->treeStorage->load('test4'));
+    $this->assertMenuLink('test5', array('has_children' => 1, 'depth' => 2), array('test1'), array('test2', 'test6'));
+    $this->assertMenuLink('test6', array('has_children' => 0, 'depth' => 3), array('test5', 'test1'));
+  }
+
+  /**
+   * Tests with hidden child links.
+   */
+  public function testMenuHiddenChildLinks() {
+    // Add some links with parent on the previous one and test some values.
+    // <tools>
+    // - test1
+    // -- test2 (hidden)
+
+    $this->addMenuLink('test1', '');
+    $this->assertMenuLink('test1', array('has_children' => 0, 'depth' => 1));
+
+    $this->addMenuLink('test2', 'test1', '<front>', array(), 'tools', array('hidden' => 1));
+    // The 1st link does not have any visible children, so has_children is 0.
+    $this->assertMenuLink('test1', array('has_children' => 0, 'depth' => 1));
+    $this->assertMenuLink('test2', array('has_children' => 0, 'depth' => 2, 'hidden' => 1), array('test1'));
+
+    // Add more links with parent on the previous one.
+    // <footer>
+    // - footerA
+    // ===============
+    // <tools>
+    // - test1
+    // -- test2 (hidden)
+    // --- test3
+    // ---- test4
+    // ----- test5
+    // ------ test6
+    // ------- test7
+    // -------- test8
+    // --------- test9
+    $this->addMenuLink('footerA', '', '<front>', array(), 'footer');
+    $visible_children = array();
+    for ($i = 3; $i <= $this->treeStorage->maxDepth(); $i++) {
+      $parent = $i - 1;
+      $this->addMenuLink("test$i", "test$parent");
+      $visible_children[] = "test$i";
+    }
+    // The 1st link does not have any visible children, so has_children is still
+    // 0. However, it has visible links below it that will be found.
+    $this->assertMenuLink('test1', array('has_children' => 0, 'depth' => 1), array(), $visible_children);
+    // This should fail since test9 would end up at greater than max depth.
+    try {
+      $this->moveMenuLink('test1', 'footerA');
+      $this->fail('Exception was not thrown');
+    }
+    catch (PluginException $e) {
+      $this->pass($e->getMessage());
+    }
+    // The opposite move should work, and change the has_children flag.
+    $this->moveMenuLink('footerA', 'test1');
+    $visible_children[] = 'footerA';
+    $this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), $visible_children);
+  }
+
+  /**
+   * Tests the loadTreeData method.
+   */
+  public function testLoadTree() {
+    $this->addMenuLink('test1', '');
+    $this->addMenuLink('test2', 'test1');
+    $this->addMenuLink('test3', 'test2');
+    $this->addMenuLink('test4');
+    $this->addMenuLink('test5', 'test4');
+
+    $data = $this->treeStorage->loadTreeData('tools', new MenuTreeParameters());
+    $tree = $data['tree'];
+    $this->assertEqual(count($tree['test1']['subtree']), 1);
+    $this->assertEqual(count($tree['test1']['subtree']['test2']['subtree']), 1);
+    $this->assertEqual(count($tree['test1']['subtree']['test2']['subtree']['test3']['subtree']), 0);
+    $this->assertEqual(count($tree['test4']['subtree']), 1);
+    $this->assertEqual(count($tree['test4']['subtree']['test5']['subtree']), 0);
+
+    $parameters = new MenuTreeParameters();
+    $parameters->setActiveTrail(array('test4', 'test5'));
+    $data = $this->treeStorage->loadTreeData('tools', $parameters);
+    $tree = $data['tree'];
+    $this->assertEqual(count($tree['test1']['subtree']), 1);
+    $this->assertFalse($tree['test1']['in_active_trail']);
+    $this->assertEqual(count($tree['test1']['subtree']['test2']['subtree']), 1);
+    $this->assertFalse($tree['test1']['subtree']['test2']['in_active_trail']);
+    $this->assertEqual(count($tree['test1']['subtree']['test2']['subtree']['test3']['subtree']), 0);
+    $this->assertFalse($tree['test1']['subtree']['test2']['subtree']['test3']['in_active_trail']);
+    $this->assertEqual(count($tree['test4']['subtree']), 1);
+    $this->assertTrue($tree['test4']['in_active_trail']);
+    $this->assertEqual(count($tree['test4']['subtree']['test5']['subtree']), 0);
+    $this->assertTrue($tree['test4']['subtree']['test5']['in_active_trail']);
+  }
+
+  /**
+   * Tests finding the subtree height with content menu links.
+   */
+  public function testSubtreeHeight() {
+
+    $storage = \Drupal::entityManager()->getStorage('menu_link_content');
+
+    // root
+    // - child1
+    // -- child2
+    // --- child3
+    // ---- child4
+    $root = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content'));
+    $root->save();
+    $child1 = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content', 'parent' => $root->getPluginId()));
+    $child1->save();
+    $child2 = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content', 'parent' => $child1->getPluginId()));
+    $child2->save();
+    $child3 = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content', 'parent' => $child2->getPluginId()));
+    $child3->save();
+    $child4 = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content', 'parent' => $child3->getPluginId()));
+    $child4->save();
+
+    $this->assertEqual($this->treeStorage->getSubtreeHeight($root->getPluginId()), 5);
+    $this->assertEqual($this->treeStorage->getSubtreeHeight($child1->getPluginId()), 4);
+    $this->assertEqual($this->treeStorage->getSubtreeHeight($child2->getPluginId()), 3);
+    $this->assertEqual($this->treeStorage->getSubtreeHeight($child3->getPluginId()), 2);
+    $this->assertEqual($this->treeStorage->getSubtreeHeight($child4->getPluginId()), 1);
+  }
+
+  /**
+   * Adds a link with the given ID and supply defaults.
+   */
+  protected function addMenuLink($id, $parent = '', $route_name = 'test', $route_parameters = array(), $menu_name = 'tools', $extra = array()) {
+    $link = array(
+      'id' => $id,
+      'menu_name' => $menu_name,
+      'route_name' => $route_name,
+      'route_parameters' => $route_parameters,
+      'title_arguments' => array(),
+      'title' => 'test',
+      'parent' => $parent,
+      'options' => array(),
+      'metadata' => array(),
+    ) + $extra;
+    $this->treeStorage->save($link);
+  }
+
+  /**
+   * Moves the link with the given ID so it's under a new parent.
+   *
+   * @param string $id
+   *   The ID of the menu link to move.
+   * @param string $new_parent
+   *   The ID of the new parent link.
+   */
+  protected function moveMenuLink($id, $new_parent) {
+    $menu_link = $this->treeStorage->load($id);
+    $menu_link['parent'] = $new_parent;
+    $this->treeStorage->save($menu_link);
+  }
+
+  /**
+   * Tests that a link's stored representation matches the expected values.
+   *
+   * @param string $id
+   *   The ID of the menu link to test
+   * @param array $expected_properties
+   *   A keyed array of column names and values like has_children and depth.
+   * @param array $parents
+   *   An ordered array of the IDs of the menu links that are the parents.
+   * @param array $children
+   *   Array of child IDs that are visible (hidden == 0).
+   */
+  protected function assertMenuLink($id, array $expected_properties, array $parents = array(), array $children = array()) {
+    $query = $this->connection->select('menu_tree');
+    $query->fields('menu_tree');
+    $query->condition('id', $id);
+    foreach ($expected_properties as $field => $value) {
+      $query->condition($field, $value);
+    }
+    $all = $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
+    $this->assertEqual(count($all), 1, "Found link $id matching all the expected properties");
+    $raw = reset($all);
+
+    // Put the current link onto the front.
+    array_unshift($parents, $raw['id']);
+
+    $query = $this->connection->select('menu_tree');
+    $query->fields('menu_tree', array('id', 'mlid'));
+    $query->condition('id', $parents, 'IN');
+    $found_parents = $query->execute()->fetchAllKeyed(0, 1);
+
+    $this->assertEqual(count($parents), count($found_parents), 'Found expected number of parents');
+    $this->assertEqual($raw['depth'], count($found_parents), 'Number of parents is the same as the depth');
+
+    $materialized_path = $this->treeStorage->getRootPathIds($id);
+    $this->assertEqual(array_values($materialized_path), array_values($parents), 'Parents match the materialized path');
+    // Check that the selected mlid values of the parents are in the correct
+    // column, including the link's own.
+    for ($i = $raw['depth']; $i >= 1; $i--) {
+      $parent_id = array_shift($parents);
+      $this->assertEqual($raw["p$i"], $found_parents[$parent_id], "mlid of parent matches at column p$i");
+    }
+    for ($i = $raw['depth'] + 1; $i <= $this->treeStorage->maxDepth(); $i++) {
+      $this->assertEqual($raw["p$i"], 0, "parent is 0 at column p$i greater than depth");
+    }
+    if ($parents) {
+      $this->assertEqual($raw['parent'], end($parents), 'Ensure that the parent field is set properly');
+    }
+    $found_children = array_keys($this->treeStorage->loadAllChildren($id));
+    // We need both these checks since the 2nd will pass if there are extra
+    // IDs loaded in $found_children.
+    $this->assertEqual(count($children), count($found_children), "Found expected number of children for $id");
+    $this->assertEqual(array_intersect($children, $found_children), $children, 'Child IDs match');
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php b/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php
new file mode 100644
index 0000000..e536f2c
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php
@@ -0,0 +1,263 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Menu\DefaultMenuLinkTreeManipulatorsTest.
+ */
+
+namespace Drupal\Tests\Core\Menu;
+
+use Drupal\Core\Menu\DefaultMenuLinkTreeManipulators;
+use Drupal\Core\Menu\MenuLinkTreeElement;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the default menu link tree manipulators.
+ *
+ * @group Drupal
+ * @group Menu
+ *
+ * @coversDefaultClass \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators
+ */
+class DefaultMenuLinkTreeManipulatorsTest extends UnitTestCase {
+
+  /**
+   * The mocked access manager.
+   *
+   * @var \Drupal\Core\Access\AccessManager|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $accessManager;
+
+  /**
+   * The mocked current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $currentUser;
+
+  /**
+   * The default menu link tree manipulators.
+   *
+   * @var \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators
+   */
+  protected $defaultMenuTreeManipulators;
+
+  /**
+   * The original menu tree build in mockTree()
+   *
+   * @var array
+   */
+  protected $originalTree = array();
+
+  /**
+   * Array of menu link instances
+   *
+   * @var \Drupal\Core\Menu\MenuLinkInterface[]
+   */
+  protected $links = array();
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Tests \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators',
+      'description' => '',
+      'group' => 'Menu',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    $this->accessManager = $this->getMockBuilder('\Drupal\Core\Access\AccessManager')
+      ->disableOriginalConstructor()->getMock();
+    $this->currentUser = $this->getMock('Drupal\Core\Session\AccountInterface');
+
+    $this->defaultMenuTreeManipulators = new DefaultMenuLinkTreeManipulators($this->accessManager, $this->currentUser);
+  }
+
+  /**
+   * Helper function to create a mock tree.
+   *
+   * This mocks a tree with the following structure:
+   * - 1
+   * - 2
+   *   - 3
+   *     - 4
+   * - 5
+   *   - 7
+   * - 6
+   * - 8
+   *
+   * With link 6 being the only external link.
+   */
+  protected function mockTree() {
+    $this->links = array(
+      1 => MenuLinkMock::create(array('id' => 'test.example1', 'route_name' => 'example1', 'title' => 'foo', 'parent' => '')),
+      2 => MenuLinkMock::create(array('id' => 'test.example2', 'route_name' => 'example2', 'title' => 'bar', 'parent' => 'test.example1', 'route_parameters' => array('foo' => 'bar'))),
+      3 => MenuLinkMock::create(array('id' => 'test.example3', 'route_name' => 'example3', 'title' => 'baz', 'parent' => 'test.example2', 'route_parameters' => array('baz' => 'qux'))),
+      4 => MenuLinkMock::create(array('id' => 'test.example4', 'route_name' => 'example4', 'title' => 'qux', 'parent' => 'test.example3')),
+      5 => MenuLinkMock::create(array('id' => 'test.example5', 'route_name' => 'example5', 'title' => 'foofoo', 'parent' => '')),
+      6 => MenuLinkMock::create(array('id' => 'test.example6', 'route_name' => '', 'url' => 'https://drupal.org/', 'title' => 'barbar', 'parent' => '')),
+      7 => MenuLinkMock::create(array('id' => 'test.example7', 'route_name' => 'example7', 'title' => 'bazbaz', 'parent' => '')),
+      8 => MenuLinkMock::create(array('id' => 'test.example8', 'route_name' => 'example8', 'title' => 'quxqux', 'parent' => '')),
+    );
+    $this->originalTree = array();
+    $this->originalTree[1] = new MenuLinkTreeElement($this->links[1], FALSE, 1, FALSE, array());
+    $this->originalTree[2] = new MenuLinkTreeElement($this->links[2], TRUE, 1, FALSE, array(
+      3 => new MenuLinkTreeElement($this->links[3], TRUE, 2, FALSE, array(
+          4 => new MenuLinkTreeElement($this->links[4], FALSE, 3, FALSE, array()),
+        )),
+    ));
+    $this->originalTree[5] = new MenuLinkTreeElement($this->links[5], TRUE, 1, FALSE, array(
+      7 => new MenuLinkTreeElement($this->links[7], FALSE, 2, FALSE, array()),
+    ));
+    $this->originalTree[6] = new MenuLinkTreeElement($this->links[6], FALSE, 1, FALSE, array());
+    $this->originalTree[8] = new MenuLinkTreeElement($this->links[8], FALSE, 1, FALSE, array());
+  }
+
+  /**
+   * Tests the generateIndexAndSort() tree manipulator.
+   *
+   * @covers ::generateIndexAndSort
+   */
+  public function testGenerateIndexAndSort() {
+    $this->mockTree();
+    $tree = $this->originalTree;
+    $tree = $this->defaultMenuTreeManipulators->generateIndexAndSort($tree);
+
+    // Validate that parent elements #1, #2, #5 and #6 exist on the root level.
+    $this->assertEquals($this->links[1]->getPluginId(), $tree['50000 foo test.example1']->link->getPluginId());
+    $this->assertEquals($this->links[2]->getPluginId(), $tree['50000 bar test.example2']->link->getPluginId());
+    $this->assertEquals($this->links[5]->getPluginId(), $tree['50000 foofoo test.example5']->link->getPluginId());
+    $this->assertEquals($this->links[6]->getPluginId(), $tree['50000 barbar test.example6']->link->getPluginId());
+    $this->assertEquals($this->links[8]->getPluginId(), $tree['50000 quxqux test.example8']->link->getPluginId());
+
+    // Verify that child element #4 is at the correct location in the hierarchy.
+    $this->assertEquals($this->links[4]->getPluginId(), $tree['50000 bar test.example2']->subtree['50000 baz test.example3']->subtree['50000 qux test.example4']->link->getPluginId());
+    // Verify that child element #7 is at the correct location in the hierarchy.
+    $this->assertEquals($this->links[7]->getPluginId(), $tree['50000 foofoo test.example5']->subtree['50000 bazbaz test.example7']->link->getPluginId());
+  }
+
+  /**
+   * Tests the checkAccess() tree manipulator.
+   *
+   * @covers ::checkAccess
+   */
+  public function testCheckAccess() {
+    // Those menu links that are non-external will have their access checks
+    // performed. 8 routes, but 1 is external, 2 already have their 'access'
+    // property set, and 1 is a child if an inaccessible menu link, so only 4
+    // calls will be made.
+    $this->accessManager->expects($this->exactly(4))
+      ->method('checkNamedRoute')
+      ->will($this->returnValueMap(array(
+        array('example1', array(), $this->currentUser, NULL, FALSE),
+        array('example2', array('foo' => 'bar'), $this->currentUser, NULL, TRUE),
+        array('example3', array('baz' => 'qux'), $this->currentUser, NULL, FALSE),
+        array('example5', array(), $this->currentUser, NULL, TRUE),
+      )));
+
+    $this->mockTree();
+    $this->originalTree[5]->subtree[7]->access = TRUE;
+    $this->originalTree[8]->access = FALSE;
+
+    $tree = $this->defaultMenuTreeManipulators->checkAccess($this->originalTree);
+
+    // Menu link 1: route without parameters, access forbidden, hence removed.
+    $this->assertFalse(array_key_exists(1, $tree));
+    // Menu link 2: route with parameters, access granted.
+    $element = $tree[2];
+    $this->assertTrue($element->access);
+    // Menu link 3: route with parameters, access forbidden, hence removed,
+    // including its children.
+    $this->assertFalse(array_key_exists(3, $tree[2]->subtree));
+    // Menu link 4: child of menu link 3, which already is removed.
+    $this->assertSame(array(), $tree[2]->subtree);
+    // Menu link 5: no route name, treated as external, hence access granted.
+    $element = $tree[5];
+    $this->assertTrue($element->access);
+    // Menu link 6: external URL, hence access granted.
+    $element = $tree[6];
+    $this->assertTrue($element->access);
+    // Menu link 7: 'access' already set.
+    $element = $tree[5]->subtree[7];
+    $this->assertTrue($element->access);
+    // Menu link 8: 'access' already set, to FALSE, hence removed.
+    $this->assertFalse(array_key_exists(8, $tree));
+  }
+
+  /**
+   * Tests the flatten() tree manipulator.
+   *
+   * @covers ::flatten
+   */
+  public function testFlatten() {
+    $this->mockTree();
+    $tree = $this->defaultMenuTreeManipulators->flatten($this->originalTree);
+    $this->assertEquals(array(1, 2, 5, 6, 8), array_keys($this->originalTree));
+    $this->assertEquals(array(1, 2, 5, 6, 8, 3, 4, 7), array_keys($tree));
+  }
+
+  /**
+   * Tests the extractSubtreeOfActiveTrail() tree manipulator.
+   *
+   * @covers ::extractSubtreeOfActiveTrail
+   */
+  public function testExtractSubtreeOfActiveTrail() {
+    // No link in the active trail.
+    $this->mockTree();
+    // Get level 0.
+    $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 0);
+    $this->assertEquals(array(1, 2, 5, 6, 8), array_keys($tree));
+    // Get level 1.
+    $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 1);
+    $this->assertEquals(array(), array_keys($tree));
+    // Get level 2.
+    $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 1);
+    $this->assertEquals(array(), array_keys($tree));
+
+    // Link 5 in the active trail.
+    $this->mockTree();
+    $this->originalTree[5]->inActiveTrail = TRUE;
+    // Get level 0.
+    $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 0);
+    $this->assertEquals(array(1, 2, 5, 6, 8), array_keys($tree));
+    // Get level 1.
+    $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 1);
+    $this->assertEquals(array(7), array_keys($tree));
+    // Get level 2.
+    $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 2);
+    $this->assertEquals(array(), array_keys($tree));
+
+    // Link 2 in the active trail.
+    $this->mockTree();
+    $this->originalTree[2]->inActiveTrail = TRUE;
+    // Get level 0.
+    $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 0);
+    $this->assertEquals(array(1, 2, 5, 6, 8), array_keys($tree));
+    // Get level 1.
+    $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 1);
+    $this->assertEquals(array(3), array_keys($tree));
+    // Get level 2.
+    $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 2);
+    $this->assertEquals(array(), array_keys($tree));
+
+    // Links 2 and 3 in the active trail.
+    $this->mockTree();
+    $this->originalTree[2]->inActiveTrail = TRUE;
+    $this->originalTree[2]->subtree[3]->inActiveTrail = TRUE;
+    // Get level 0.
+    $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 0);
+    $this->assertEquals(array(1, 2, 5, 6, 8), array_keys($tree));
+    // Get level 1.
+    $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 1);
+    $this->assertEquals(array(3), array_keys($tree));
+    // Get level 2.
+    $tree = $this->defaultMenuTreeManipulators->extractSubtreeOfActiveTrail($this->originalTree, 2);
+    $this->assertEquals(array(4), array_keys($tree));
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Menu/MenuActiveTrailTest.php b/core/tests/Drupal/Tests/Core/Menu/MenuActiveTrailTest.php
new file mode 100644
index 0000000..69f5610
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Menu/MenuActiveTrailTest.php
@@ -0,0 +1,172 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_link\Tests\MenuActiveTrailTest.
+ */
+
+namespace Drupal\Tests\Core\Menu;
+
+use Drupal\Core\Menu\MenuActiveTrail;
+use Drupal\Core\Routing\CurrentRouteMatch;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\ParameterBag;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Tests the active menu trail service.
+ *
+ * @group Drupal
+ * @group Menu
+ *
+ * @coversDefaultClass \Drupal\Core\Menu\MenuActiveTrail
+ */
+class MenuActiveTrailTest extends UnitTestCase {
+
+  /**
+   * The tested active menu trail service.
+   *
+   * @var \Drupal\Core\Menu\MenuActiveTrail
+   */
+  protected $menuActiveTrail;
+
+  /**
+   * The test request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * The current route match service.
+   *
+   * @var \Drupal\Core\Routing\CurrentRouteMatch;
+   */
+  protected $currentRouteMatch;
+
+  /**
+   * The mocked menu link manager.
+   *
+   * @var \Drupal\Core\Menu\MenuLinkManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $menuLinkManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Tests \Drupal\Core\Menu\MenuActiveTrail',
+      'description' => '',
+      'group' => 'Menu',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    $this->requestStack = new RequestStack();
+    $this->currentRouteMatch = new CurrentRouteMatch($this->requestStack);
+    $this->menuLinkManager = $this->getMock('Drupal\Core\Menu\MenuLinkManagerInterface');
+
+    $this->menuActiveTrail = new MenuActiveTrail($this->menuLinkManager, $this->currentRouteMatch);
+  }
+
+  /**
+   * Provides test data for all test methods.
+   */
+  public function provider() {
+    $data = array();
+
+    $mock_route = new Route('');
+
+    $request = (new Request());
+    $request->attributes->set(RouteObjectInterface::ROUTE_NAME, 'baby_llama');
+    $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, $mock_route);
+    $request->attributes->set('_raw_variables', new ParameterBag(array()));
+
+    $link_1 = MenuLinkMock::create(array('id' => 'baby_llama_link_1', 'route_name' => 'baby_llama', 'title' => 'Baby llama', 'parent' => 'mama_llama_link'));
+    $link_2 = MenuLinkMock::create(array('id' => 'baby_llama_link_2', 'route_name' => 'baby_llama', 'title' => 'Baby llama', 'parent' => 'papa_llama_link'));
+
+    // @see \Drupal\Core\Menu\MenuLinkManagerInterface::getParentIds()
+    $link_1_parent_ids = array('baby_llama_link_1', 'mama_llama_link', '');
+    $empty_active_trail = array('');
+
+    $link_1__active_trail_cache_key = 'menu_trail.baby_llama_link_1|mama_llama_link|';
+    $empty_active_trail_cache_key = 'menu_trail.';
+
+    // No active link is returned when zero links match the current route.
+    $data[] = array($request, array(), $this->randomName(), NULL, $empty_active_trail, $empty_active_trail_cache_key);
+
+    // The first (and only) matching link is returned when one link matches the
+    // current route.
+    $data[] = array($request, array('baby_llama_link_1' => $link_1), $this->randomName(), $link_1, $link_1_parent_ids, $link_1__active_trail_cache_key);
+
+    // The first of multiple matching links is returned when multiple links
+    // match the current route, where "first" is determined by sorting by key.
+    $data[] = array($request, array('baby_llama_link_1' => $link_1, 'baby_llama_link_2' => $link_2), $this->randomName(), $link_1, $link_1_parent_ids, $link_1__active_trail_cache_key);
+
+    // No active link is returned in case of a 403.
+    $request = (new Request());
+    $request->attributes->set('_exception_statuscode', 403);
+    $data[] = array($request, FALSE, $this->randomName(), NULL, $empty_active_trail, $empty_active_trail_cache_key);
+
+    // No active link is returned when the route name is missing.
+    $request = (new Request());
+    $data[] = array($request, FALSE, $this->randomName(), NULL, $empty_active_trail, $empty_active_trail_cache_key);
+
+    return $data;
+  }
+
+  /**
+   * Tests getActiveLink().
+   *
+   * @covers ::getActiveLink()
+   * @dataProvider provider
+   */
+  public function testGetActiveLink(Request $request, $links, $menu_name, $expected_link) {
+    $this->requestStack->push($request);
+    if ($links !== FALSE) {
+      $this->menuLinkManager->expects($this->exactly(2))
+        ->method('loadLinksbyRoute')
+        ->will($this->returnValue($links));
+    }
+    // Test with menu name.
+    $this->assertSame($expected_link, $this->menuActiveTrail->getActiveLink($menu_name));
+    // Test without menu name.
+    $this->assertSame($expected_link, $this->menuActiveTrail->getActiveLink());
+  }
+
+  /**
+   * Tests getActiveTrailIds().
+   *
+   * @covers ::getActiveTrailIds()
+   * @covers ::getActiveTrailCacheKey()
+   * @dataProvider provider
+   */
+  public function testGetActiveTrailIds(Request $request, $links, $menu_name, $expected_link, $expected_trail, $expected_cache_key) {
+    $expected_trail_ids = array_combine($expected_trail, $expected_trail);
+
+    $this->requestStack->push($request);
+    if ($links !== FALSE) {
+      $this->menuLinkManager->expects($this->exactly(2))
+        ->method('loadLinksbyRoute')
+        ->will($this->returnValue($links));
+      if ($expected_link !== NULL) {
+        $this->menuLinkManager->expects($this->exactly(2))
+          ->method('getParentIds')
+          ->will($this->returnValueMap(array(
+            array($expected_link->getPluginId(), $expected_trail_ids),
+          )));
+      }
+    }
+
+    $this->assertSame($expected_trail_ids, $this->menuActiveTrail->getActiveTrailIds($menu_name));
+    $this->assertSame($expected_cache_key, $this->menuActiveTrail->getActiveTrailCacheKey($menu_name));
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Menu/MenuLinkMock.php b/core/tests/Drupal/Tests/Core/Menu/MenuLinkMock.php
new file mode 100644
index 0000000..5f40c58
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Menu/MenuLinkMock.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Menu\MenuLinkMock
+ */
+
+namespace Drupal\Tests\Core\Menu;
+
+use Drupal\Core\Menu\MenuLinkBase;
+
+class MenuLinkMock extends MenuLinkBase {
+
+  protected static $defaults = array(
+    'menu_name' => 'mock',
+    'route_name' => 'MUST BE PROVIDED',
+    'route_parameters' => array(),
+    'url' => '',
+    'title' => 'MUST BE PROVIDED',
+    'title_arguments' => array(),
+    'title_context' => '',
+    'description' => '',
+    'parent' => 'MUST BE PROVIDED',
+    'weight' => '0',
+    'options' => array(),
+    'expanded' => '0',
+    'hidden' => '0',
+    'discovered' => '1',
+    'provider' => 'simpletest',
+    'metadata' => array(),
+    'class' => 'Drupal\\Tests\\Core\Menu\\MenuLinkMock',
+    'form_class' => 'Drupal\\Core\\Menu\\Form\\MenuLinkDefaultForm',
+    'id' => 'MUST BE PROVIDED',
+  );
+
+  /**
+   * Create an instance from a definition with at least id, title, route_name.
+   */
+  public static function create($definition) {
+    return new static(array(), $definition['id'], $definition + static::$defaults);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTitle() {
+    return $this->pluginDefinition['title'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    if ($this->pluginDefinition['description']) {
+      return $this->pluginDefinition['description'];
+    }
+    return '';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateLink(array $new_definition_values, $persist) {
+    // No-op.
+    return $this->pluginDefinition;
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Menu/MenuLinkTreeElementTest.php b/core/tests/Drupal/Tests/Core/Menu/MenuLinkTreeElementTest.php
new file mode 100644
index 0000000..39cda99
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Menu/MenuLinkTreeElementTest.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Menu\MenuLinkTreeElementTest.
+ */
+
+namespace Drupal\Tests\Core\Menu;
+
+use Drupal\Core\Menu\MenuLinkTreeElement;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the menu link tree element value object.
+ *
+ * @group Drupal
+ * @group Menu
+ *
+ * @coversDefaultClass \Drupal\Core\Menu\MenuLinkTreeElement
+ */
+class MenuLinkTreeElementTest extends UnitTestCase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Tests \Drupal\Core\Menu\MenuLinkTreeElement',
+      'description' => '',
+      'group' => 'Menu',
+    );
+  }
+
+  /**
+   * Tests construction.
+   *
+   * @covers ::__construct
+   */
+  public function testConstruction() {
+    $link = array();
+    $item = new MenuLinkTreeElement($link, FALSE, 3, FALSE, array());
+    $this->assertSame($link, $item->link);
+    $this->assertSame(FALSE, $item->hasChildren);
+    $this->assertSame(3, $item->depth);
+    $this->assertSame(FALSE, $item->inActiveTrail);
+    $this->assertSame(array(), $item->subtree);
+  }
+
+  /**
+   * Tests count().
+   *
+   * @covers ::count
+   */
+  public function testCount() {
+    $link_1 = array();
+    $link_2 = array();
+    $child_item = new MenuLinkTreeElement($link_2, FALSE, 2, FALSE, array());
+    $parent_item = new MenuLinkTreeElement($link_1, FALSE, 2, FALSE, array($child_item));
+    $this->assertSame(1, $child_item->count());
+    $this->assertSame(2, $parent_item->count());
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Menu/MenuLinkTreeParametersTest.php b/core/tests/Drupal/Tests/Core/Menu/MenuLinkTreeParametersTest.php
new file mode 100644
index 0000000..f0118db
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Menu/MenuLinkTreeParametersTest.php
@@ -0,0 +1,162 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Menu\MenuLinkTreeParametersTest.
+ */
+
+namespace Drupal\Tests\Core\Menu;
+
+use Drupal\Core\Menu\MenuTreeParameters;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the menu link tree parameters value object.
+ *
+ * @group Drupal
+ * @group Menu
+ *
+ * @coversDefaultClass \Drupal\Core\Menu\MenuTreeParameters
+ */
+class MenuLinkTreeParametersTest extends UnitTestCase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Tests Drupal\Core\Menu\MenuLinkTreeParameters',
+      'description' => '',
+      'group' => 'Menu',
+    );
+  }
+
+  /**
+   * Provides test data for testSetMinDepth().
+   */
+  public function providerTestSetMinDepth() {
+    $data = array();
+
+    // Valid values at the extremes and in the middle.
+    $data[] = array(1, 1);
+    $data[] = array(2, 2);
+    $data[] = array(9, 9);
+
+    // Invalid values are mapped to the closest valid value.
+    $data[] = array(-10000, 1);
+    $data[] = array(0, 1);
+    // … except for those invalid values that reach beyond the maximum depth,
+    // because MenuTreeParameters is a value object and hence cannot depend
+    // on anything; to know the actual maximum depth, it'd have to depend on the
+    // MenuTreeStorage service.
+    $data[] = array(10, 10);
+    $data[] = array(100000, 100000);
+
+    return $data;
+  }
+
+  /**
+   * Tests setMinDepth().
+   *
+   * @covers ::setMinDepth
+   * @dataProvider providerTestSetMinDepth
+   */
+  public function testSetMinDepth($min_depth, $expected) {
+    $parameters = new MenuTreeParameters();
+    $parameters->setMinDepth($min_depth);
+    $this->assertEquals($expected, $parameters->minDepth);
+  }
+
+  /**
+   * Tests addExpandedParents().
+   *
+   * @covers ::addExpandedParents
+   */
+  public function testAddExpanded() {
+    $parameters = new MenuTreeParameters();
+
+    // Verify default value.
+    $this->assertEquals(array(), $parameters->expandedParents);
+
+    // Add actual menu link plugin IDs to be expanded.
+    $parameters->addExpandedParents(array('foo', 'bar', 'baz'));
+    $this->assertEquals(array('foo', 'bar', 'baz'), $parameters->expandedParents);
+
+    // Add additional menu link plugin IDs; they should be merged, not replacing
+    // the old ones.
+    $parameters->addExpandedParents(array('qux', 'quux'));
+    $this->assertEquals(array('foo', 'bar', 'baz', 'qux', 'quux'), $parameters->expandedParents);
+
+    // Add pre-existing menu link plugin IDs; they should not be added again;
+    // this is a set.
+    $parameters->addExpandedParents(array('bar', 'quux'));
+    $this->assertEquals(array('foo', 'bar', 'baz', 'qux', 'quux'), $parameters->expandedParents);
+  }
+
+  /**
+   * Tests addCondition().
+   *
+   * @covers ::addCondition
+   */
+  public function testAddCondition() {
+    $parameters = new MenuTreeParameters();
+
+    // Verify default value.
+    $this->assertEquals(array(), $parameters->conditions);
+
+    // Add a condition.
+    $parameters->addCondition('expanded', 1);
+    $this->assertEquals(array('expanded' => 1), $parameters->conditions);
+
+    // Add another condition.
+    $parameters->addCondition('has_children', 0);
+    $this->assertEquals(array('expanded' => 1, 'has_children' => 0), $parameters->conditions);
+
+    // Add a condition with an operator.
+    $parameters->addCondition('provider', array('module1', 'module2'), 'IN');
+    $this->assertEquals(array('expanded' => 1, 'has_children' => 0, 'provider' => array(array('module1', 'module2'), 'IN')), $parameters->conditions);
+
+    // Add another condition with an operator.
+    $parameters->addCondition('id', 1337, '<');
+    $this->assertEquals(array('expanded' => 1, 'has_children' => 0, 'provider' => array(array('module1', 'module2'), 'IN'), 'id' => array(1337, '<')), $parameters->conditions);
+
+    // It's impossible to add two conditions on the same field; in that case,
+    // the old condition will be overwritten.
+    $parameters->addCondition('provider', 'other_module');
+    $this->assertEquals(array('expanded' => 1, 'has_children' => 0, 'provider' => 'other_module', 'id' => array(1337, '<')), $parameters->conditions);
+  }
+
+  /**
+   * Tests excludeHiddenLinks().
+   *
+   * @covers ::excludeHiddenLinks
+   */
+  public function testExcludeHiddenLinks() {
+    $parameters = new MenuTreeParameters();
+    $parameters->excludeHiddenLinks();
+    $this->assertEquals(0, $parameters->conditions['hidden']);
+  }
+
+  /**
+   * Tests topLevelOnly().
+   *
+   * @covers ::topLevelOnly
+   */
+  public function testTopLevelOnly() {
+    $parameters = new MenuTreeParameters();
+    $parameters->topLevelOnly();
+    $this->assertEquals(1, $parameters->maxDepth);
+  }
+
+  /**
+   * Tests excludeRoot().
+   *
+   * @covers ::excludeRoot
+   */
+  public function testExcludeRoot() {
+    $parameters = new MenuTreeParameters();
+    $parameters->excludeRoot();
+    $this->assertEquals(1, $parameters->minDepth);
+  }
+
+}
