From 8bb62da27d7d0f97168a9f868ceabc152e00cbb8 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Tue, 15 Jul 2014 09:15:14 +0100
Subject: [PATCH] Issue #2301239 by pwolanin, dawehner, Wim Leers,
 effulgentsia, joelpittet, larowlan, xjm, YesCT, kgoel, victoru, berdir,
 likin, and plach: MenuLinkNG part1 (no UI or conversions): plugins (static +
 MenuLinkContent) + MenuLinkManager + MenuTreeStorage.

---
 core/core.services.yml                        |   17 +
 core/lib/Drupal/Core/Menu/MenuLinkBase.php    |  197 +++
 core/lib/Drupal/Core/Menu/MenuLinkDefault.php |   88 +
 .../Drupal/Core/Menu/MenuLinkInterface.php    |  233 +++
 core/lib/Drupal/Core/Menu/MenuLinkManager.php |  416 +++++
 .../Core/Menu/MenuLinkManagerInterface.php    |  200 +++
 .../Drupal/Core/Menu/MenuTreeParameters.php   |  222 +++
 core/lib/Drupal/Core/Menu/MenuTreeStorage.php | 1441 +++++++++++++++++
 .../Core/Menu/MenuTreeStorageInterface.php    |  278 ++++
 .../Core/Menu/StaticMenuLinkOverrides.php     |  163 ++
 .../Menu/StaticMenuLinkOverridesInterface.php |   87 +
 .../Core/Plugin/CachedDiscoveryClearer.php    |    2 +-
 .../Drupal/Core/Plugin/PluginManagerPass.php  |    4 +-
 .../menu_link_content.info.yml                |    6 +
 .../menu_link_content.module                  |   17 +
 .../src/Entity/MenuLinkContent.php            |  385 +++++
 .../src/Entity/MenuLinkContentInterface.php   |  152 ++
 .../src/MenuLinkContentAccessController.php   |   68 +
 .../src/Plugin/Menu/MenuLinkContent.php       |  250 +++
 .../src/Tests/Menu/MenuTreeStorageTest.php    |  389 +++++
 .../Core/Menu/StaticMenuLinkOverridesTest.php |  219 +++
 21 files changed, 4832 insertions(+), 2 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Menu/MenuLinkBase.php
 create mode 100644 core/lib/Drupal/Core/Menu/MenuLinkDefault.php
 create mode 100644 core/lib/Drupal/Core/Menu/MenuLinkInterface.php
 create mode 100644 core/lib/Drupal/Core/Menu/MenuLinkManager.php
 create mode 100644 core/lib/Drupal/Core/Menu/MenuLinkManagerInterface.php
 create mode 100644 core/lib/Drupal/Core/Menu/MenuTreeParameters.php
 create mode 100644 core/lib/Drupal/Core/Menu/MenuTreeStorage.php
 create mode 100644 core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php
 create mode 100644 core/lib/Drupal/Core/Menu/StaticMenuLinkOverrides.php
 create mode 100644 core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php
 create mode 100644 core/modules/menu_link_content/menu_link_content.info.yml
 create mode 100644 core/modules/menu_link_content/menu_link_content.module
 create mode 100644 core/modules/menu_link_content/src/Entity/MenuLinkContent.php
 create mode 100644 core/modules/menu_link_content/src/Entity/MenuLinkContentInterface.php
 create mode 100644 core/modules/menu_link_content/src/MenuLinkContentAccessController.php
 create mode 100644 core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php
 create mode 100644 core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php
 create mode 100644 core/tests/Drupal/Tests/Core/Menu/StaticMenuLinkOverridesTest.php

diff --git a/core/core.services.yml b/core/core.services.yml
index ac4f816c9f8e..a43f90c24215 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,9 @@ 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']
   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 +289,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_stack:
     class: Symfony\Component\HttpFoundation\RequestStack
     tags:
diff --git a/core/lib/Drupal/Core/Menu/MenuLinkBase.php b/core/lib/Drupal/Core/Menu/MenuLinkBase.php
new file mode 100644
index 000000000000..438d862bf4ca
--- /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\Component\Plugin\Exception\PluginException;
+use Drupal\Component\Utility\String;
+use Drupal\Core\Plugin\PluginBase;
+use Drupal\Core\Url;
+
+/**
+ * Defines a base menu link class.
+ */
+abstract class MenuLinkBase extends PluginBase implements MenuLinkInterface {
+
+  /**
+   * The list of definition values where an override is allowed.
+   *
+   * The keys are definition names. The values are ignored.
+   *
+   * @var array
+   */
+  protected $overrideAllowed = array();
+
+  /**
+   * {@inheritdoc}
+   */
+  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 (bool) $this->getTranslateRoute();
+  }
+
+  /**
+   * {@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(String::format('Menu link plugin with ID @id does not support deletion', array('@id' => $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 000000000000..0439937e78ff
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkDefault.php
@@ -0,0 +1,88 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuLinkDefault.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a default implementation for menu link 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 (bool) $this->staticOverride->loadOverride($this->getPluginId());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateLink(array $new_definition_values, $persist) {
+    // Filter the list of updates to only those that are allowed.
+    $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 000000000000..6ab14342e33d
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkInterface.php
@@ -0,0 +1,233 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuLinkInterface.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Component\Plugin\DerivativeInspectionInterface;
+
+/**
+ * Defines an interface for classes providing a type of menu link.
+ */
+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.
+   *
+   * @return string
+   *   The description of the menu link.
+   */
+  public function getDescription();
+
+  /**
+   * Returns the menu name of the menu link.
+   *
+   * @return string
+   *   The menu name of the menu link.
+   */
+  public function getMenuName();
+
+  /**
+   * Returns the provider (module name) of the menu link.
+   *
+   * @return string
+   *   The provider of the menu link.
+   */
+  public function getProvider();
+
+  /**
+   * Returns the plugin ID of the menu link's parent, or an empty string.
+   *
+   * @return string
+   *   The parent plugin ID.
+   */
+  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
+   *   (optional) If TRUE, add the link description as the title attribute if
+   *   the description is not empty.
+   *
+   * @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 the definition values for a menu link.
+   *
+   * 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 MenuLinkManagerInterface::updateDefinition().
+   *
+   * @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);
+
+  /**
+   * Deletes a menu link.
+   *
+   * In general, this method should not be called directly, but will be called
+   * automatically from MenuLinkManagerInterface::removeDefinition().
+   *
+   * This method will only delete the link from any additional storage, but not
+   * from the plugin.manager.menu.link 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.
+   *
+   * @todo Add a code example. https://www.drupal.org/node/2302849
+   *
+   * @return string
+   *   A class that implements \Drupal\Core\Menu\Form\MenuLinkFormInterface.
+   */
+  public function getFormClass();
+
+  /**
+   * Returns route information for a route to delete the menu link.
+   *
+   * @return array|null
+   *   An array with keys route_name and route_parameters, or NULL if there is
+   *   no route (e.g. when the link is not deletable).
+   */
+  public function getDeleteRoute();
+
+  /**
+   * Returns route information for a custom edit form for the menu link.
+   *
+   * 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|null
+   *   An array with keys route_name and route_parameters, or NULL if there is
+   *   no route because there is no custom edit route for this instance.
+   */
+  public function getEditRoute();
+
+  /**
+   * Returns route information for a route to translate the menu link.
+   *
+   * @return array
+   *   An array with keys route_name and route_parameters, or NULL if there is
+   *   no route (e.g. when the link is not translatable).
+   */
+  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 000000000000..ab1f5ddd5197
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkManager.php
@@ -0,0 +1,416 @@
+<?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 {
+
+  /**
+   * Provides some default values for the definition of all menu link plugins.
+   *
+   * @todo Decide how to keep these field definitions in sync.
+   *   https://www.drupal.org/node/2302085
+   *
+   * @var array
+   */
+  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. You can specify placeholders like on
+    // any translatable string and the values in title_arguments.
+    'title' => '',
+    // The values for the menu link placeholders.
+    'title_arguments' => array(),
+    // A context for the title string.
+    // @see \Drupal\Core\StringTranslation\TranslationInterface::translate()
+    '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\MenuLinkManager object.
+   *
+   * @param \Drupal\Core\Menu\MenuTreeStorageInterface $tree_storage
+   *   The menu link tree storage.
+   * @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $overrides
+   *   The 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, the logic can be added by replacing or
+   * extending this method.
+   *
+   * @param array $definition
+   *   The definition to be processed and modified by reference.
+   * @param $plugin_id
+   *   The ID of the plugin this definition is being used for.
+   */
+  protected function processDefinition(array &$definition, $plugin_id) {
+    $definition = NestedArray::mergeDeep($this->defaults, $definition);
+    // Typecast so NULL, no parent, will be an empty string since the parent ID
+    // should be a string.
+    $definition['parent'] = (string) $definition['parent'];
+    $definition['id'] = $plugin_id;
+  }
+
+  /**
+   * Instantiates if necessary and returns 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.
+   *
+   * @return \Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator
+   *   A plugin discovery instance.
+   */
+  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.
+    // @todo Address what to do with an invalid plugin.
+    //   https://www.drupal.org/node/2302623
+    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(String::format('@plugin_id could not be found', array('@plugin_id' => $plugin_id)));
+    }
+    return $definition;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasDefinition($plugin_id) {
+    return (bool) $this->getDefinition($plugin_id, FALSE);
+  }
+
+  /**
+   * Returns a pre-configured menu link plugin instance.
+   *
+   * @param string $plugin_id
+   *   The ID of the plugin being instantiated.
+   * @param array $configuration
+   *   An array of configuration relevant to the plugin instance.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkInterface
+   *   A menu link instance.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   If the instance cannot be created, such as if the ID is invalid.
+   */
+  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']);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  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->isDeletable()) {
+        $this->deleteInstance($instance, TRUE);
+      }
+      elseif ($instance->isResetable()) {
+        $new_instance = $this->resetInstance($instance);
+        $affected_menus[$new_instance->getMenuName()] = $new_instance->getMenuName();
+      }
+    }
+  }
+
+  /**
+   * Deletes a specific instance.
+   *
+   * @param \Drupal\Core\Menu\MenuLinkInterface $instance
+   *   The plugin instance to be deleted.
+   * @param bool $persist
+   *   If TRUE, calls MenuLinkInterface::deleteLink() on the instance.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   If the plugin instance does not support deletion.
+   */
+  protected function deleteInstance(MenuLinkInterface $instance, $persist) {
+    $id = $instance->getPluginId();
+    if ($instance->isDeletable()) {
+      if ($persist) {
+        $instance->deleteLink();
+      }
+    }
+    else {
+      throw new PluginException(String::format('Menu link plugin with ID @id does not support deletion', array('@id' => $id)));
+    }
+    $this->treeStorage->delete($id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function removeDefinition($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 addDefinition($id, array $definition) {
+    if ($this->treeStorage->load($id) || $id === '') {
+      throw new PluginException(String::format('The ID @id already exists as a plugin definition or is not valid', array('@id' => $id)));
+    }
+    // Add defaults, 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 updateDefinition($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 resettable.
+   */
+  protected function resetInstance(MenuLinkInterface $instance) {
+    $id = $instance->getPluginId();
+
+    if (!$instance->isResetable()) {
+      throw new PluginException(String::format('Menu link %id is not resettable', 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 000000000000..3507a9205bfc
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuLinkManagerInterface.php
@@ -0,0 +1,200 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuLinkManagerInterface.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Component\Plugin\PluginManagerInterface;
+
+/**
+ * Defines an interface for managing menu links and storing their definitions.
+ *
+ * Menu link managers support both automatic plugin definition discovery and
+ * manually maintaining plugin definitions.
+ *
+ * MenuLinkManagerInterface::updateDefinition() can be used to update a single
+ * menu link's definition and pass this onto the menu storage without requiring
+ * a full MenuLinkManagerInterface::rebuild().
+ *
+ * Implementations that do not use automatic discovery should call
+ * MenuLinkManagerInterface::addDefinition() or
+ * MenuLinkManagerInterface::removeDefinition() when they add or remove links,
+ * and MenuLinkManagerInterface::updateDefinition() to update links they have
+ * already defined.
+ */
+interface MenuLinkManagerInterface extends PluginManagerInterface {
+
+  /**
+   * Triggers discovery, save, and cleanup of discovered links.
+   */
+  public function rebuild();
+
+  /**
+   * Deletes all links having a certain menu name.
+   *
+   * If a link is not deletable but is resettable, the link will be reset to have
+   * its original menu name, under the assumption that the original menu is not
+   * the one we are deleting it from. Note that when resetting, if the original
+   * menu name is the same as the menu name passed to this method, the link will
+   * not be moved or deleted.
+   *
+   * @param string $menu_name
+   *   The name of the menu whose links will be deleted or reset.
+   */
+  public function deleteLinksInMenu($menu_name);
+
+  /**
+   * Removes a single link definition from the menu tree storage.
+   *
+   * This is used for plugins not found through discovery to remove definitions.
+   *
+   * @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
+   *   Thrown if the $id is not a valid, existing, plugin ID or if the link
+   *   cannot be deleted.
+   */
+  public function removeDefinition($id, $persist = TRUE);
+
+  /**
+   * Loads 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 plugin ID.
+   */
+  public function loadLinksByRoute($route_name, array $route_parameters = array(), $menu_name = NULL);
+
+  /**
+   * Adds a new menu link definition to the menu tree storage.
+   *
+   * Use this function when you know there is no entry in the tree. This is
+   * used for plugins not found through discovery to add new definitions.
+   *
+   * @param string $id
+   *   The plugin ID for the new menu link definition that is being added.
+   * @param array $definition
+   *   The values of the link definition.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkInterface
+   *   A plugin instance created using the newly added definition.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   Thrown when the $id is not valid or is an already existing plugin ID.
+   */
+  public function addDefinition($id, array $definition);
+
+  /**
+   * Updates the values for a menu link definition in the menu tree storage.
+   *
+   * This will update the definition for a discovered menu link without the
+   * need for a full rebuild. It is also used for plugins not found through
+   * discovery to update definitions.
+   *
+   * @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::updateDefinition()
+   *   on the plugin that is being updated.
+   *
+   * @return \Drupal\Core\Menu\MenuLinkInterface
+   *   A plugin instance created using the updated definition.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   Thrown if the $id is not a valid, existing, plugin ID.
+   */
+  public function updateDefinition($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
+   *   Thrown if the $id is not a 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 all menus.
+   *
+   * @return int
+   *   The number of menu links in the named menu, or in all menus if the
+   *   menu name is 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);
+
+  /**
+   * Determines 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/MenuTreeParameters.php b/core/lib/Drupal/Core/Menu/MenuTreeParameters.php
new file mode 100644
index 000000000000..23a0992dbd58
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuTreeParameters.php
@@ -0,0 +1,222 @@
+<?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 and define an
+ *   interface instead of using the concrete class to type hint.
+ *   https://www.drupal.org/node/2302041
+ */
+class MenuTreeParameters {
+
+  /**
+   * A menu link plugin ID that should be used as the root.
+   *
+   * By default the root ID of empty string '' is used. However, 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 relative to the root.
+   *
+   * 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 relative to the root.
+   *
+   * @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();
+
+  /**
+   * The IDs from the currently active menu link to the root of the whole tree.
+   *
+   * This is 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.
+   *
+   * @var string[]
+   */
+  public $activeTrail = array();
+
+  /**
+   * The conditions used to restrict which links are loaded.
+   *
+   * An associative array of custom query condition key/value pairs.
+   *
+   * @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 empty string '' to use the root of the whole
+   *   tree.
+   *
+   * @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
+   *   (optional) The comparison operator, such as =, <, or >=. It also accepts
+   *   more complex options such as IN, LIKE, or BETWEEN. If NULL, defaults to
+   *   the = operator.
+   *
+   * @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 normal root ID is the empty string, '', which does not correspond to an
+   * actual menu link. Hence when loading a menu link tree without specifying a
+   * custom root the tree will start at the children even if this method has not
+   * been called.
+   *
+   * @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 000000000000..6741d3540aba
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuTreeStorage.php
@@ -0,0 +1,1441 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuTreeStorage.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Component\Plugin\Exception\PluginException;
+use Drupal\Component\Utility\String;
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Database\Database;
+use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\Core\Database\SchemaObjectExistsException;
+
+/**
+ * Provides a menu 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.
+   *
+   * An array of plugin definition arrays, keyed by plugin ID.
+   *
+   * @var array
+   */
+  protected $definitions = array();
+
+  /**
+   * List of serialized fields.
+   *
+   * @var array
+   */
+  protected $serializedFields;
+
+  /**
+   * List of plugin definition fields.
+   *
+   * @todo Decide how to keep these field definitions in sync.
+   *   https://www.drupal.org/node/2302085
+   *
+   * @see \Drupal\Core\Menu\MenuLinkManager::$defaults
+   *
+   * @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');
+      // Starting from links with the greatest depth will minimize the amount
+      // of re-parenting done by the menu storage.
+      $query->orderBy('depth', 'DESC');
+      $result = $query->execute()->fetchCol();
+    }
+    else {
+      $result = array();
+    }
+
+    // Remove all such items.
+    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();
+    // Every item in the cache bin should have one of the menu cache tags but it
+    // is not guaranteed, so invalidate everything in the bin.
+    $this->menuCacheBackend->invalidateAll();
+  }
+
+  /**
+   * Purges multiple menu links that no longer exist.
+   *
+   * @param array $ids
+   *   An array of menu link IDs.
+   */
+  protected function purgeMultiple(array $ids) {
+    $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
+   *   Thrown 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;
+  }
+
+  /**
+   * Saves a link without clearing caches.
+   *
+   * @param array $link
+   *   A definition, according to $definitionFields, for a
+   *   \Drupal\Core\Menu\MenuLinkInterface plugin.
+   *
+   * @return array
+   *   The menu names affected by the save operation. This will be one menu
+   *   name if the link is saved to the sane menu, or two if it is saved to a
+   *   new menu.
+   *
+   * @throws \Exception
+   *   Thrown if the storage back-end does not exist and could not be created.
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   Thrown 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?
+    //   https://www.drupal.org/node/2302137
+    $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;
+  }
+
+  /**
+   * Fills in all the fields the database save needs, using the link definition.
+   *
+   * @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);
+    // Sort the route parameters so that the query string will be the same.
+    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.
+      //   https://www.drupal.org/node/2302149
+      if ($original) {
+        $limit = $this->maxDepth() - $this->doFindChildrenRelativeDepth($original) - 1;
+      }
+      else {
+        $limit = $this->maxDepth() - 1;
+      }
+      if ($parent['depth'] > $limit) {
+        throw new PluginException(String::format('The link with ID @id or its children exceeded the maximum depth of @depth', array('@id' => $link['id'], '@depth' => $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;
+    }
+  }
+
+  /**
+   * Re-parents a link's children when the link itself is moved.
+   *
+   * @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 find the parent of.
+   * @param array|false $original
+   *   The original link that might be used to find the parent if the parent
+   *   is not set on the $link, or FALSE if the original could not be loaded.
+   *
+   * @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;
+  }
+
+  /**
+   * Sets has_children 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();
+    }
+  }
+
+  /**
+   * Prepares 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.
+         https://www.drupal.org/node/2302165
+    $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) {
+    // Sort the route parameters so that the query string will be the same.
+    asort($route_parameters);
+    // Since this will be urlencoded, it's safe to store and match against a
+    // text field.
+    // @todo Standardize an efficient way to load by route name and parameters
+    //   in place of system path. https://www.drupal.org/node/2302139
+    $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) {
+    $missing_ids = array_diff($ids, array_keys($this->definitions));
+
+    if ($missing_ids) {
+      $query = $this->connection->select($this->table, $this->options);
+      $query->fields($this->table, $this->definitionFields());
+      $query->condition('id', $missing_ids, 'IN');
+      $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC);
+      foreach ($loaded as $id => $link) {
+        $this->definitions[$id] = $this->prepareLink($link);
+      }
+    }
+    return array_intersect_key($this->definitions, array_flip($ids));
+  }
+
+  /**
+   * {@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 all table fields for 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.
+    //   https://www.drupal.org/node/2302043
+    $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 is being used more
+      //   than once per page load. https://www.drupal.org/node/2302185
+      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? https://www.drupal.org/node/2302187
+    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.
+   *
+   * @param string $id
+   *   The definition ID.
+   * @param array $children
+   *   An array of IDs of child links collected by parent ID.
+   * @param array $links
+   *   An array of all definitions keyed by ID.
+   */
+  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);
+    $tree_cid = "tree-data:$menu_name:" . 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 \Drupal\Core\Menu\MenuTreeParameters $parameters
+   *   The parameters to determine which menu links to be loaded into a tree.
+   *   This method will set the absolute minimum depth, which is used in
+   *   MenuTreeStorage::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
+    // omitted, 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 we 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 by reference.
+   *
+   * @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));
+  }
+
+  /**
+   * Collects all the route names and definitions.
+   *
+   * @param array $tree
+   *   A menu link tree from MenuTreeStorage::doBuildTreeData()
+   * @param array $definitions
+   *   The collected definitions which are populated by reference.
+   *
+   * @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 $this->table. This array must be ordered
+   *   depth-first. MenuTreeStorage::loadTreeData() includes 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.
+   *
+   * @see \Drupal\Core\Menu\MenuTreeStorage::loadTreeData()
+   */
+  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;
+  }
+
+  /**
+   * Determines serialized fields in the storage.
+   *
+   * @return array
+   *   A list of fields that are serialized in the database.
+   */
+  protected function serializedFields() {
+    if (empty($this->serializedFields)) {
+      $schema = static::schemaDefinition();
+      foreach ($schema['fields'] as $name => $field) {
+        if (!empty($field['serialize'])) {
+          $this->serializedFields[] = $name;
+        }
+      }
+    }
+    return $this->serializedFields;
+  }
+
+  /**
+   * Determines fields that are part of the plugin definition.
+   *
+   * @return array
+   *   The list of the subset of fields that are part of the plugin definition.
+   */
+  protected function definitionFields() {
+    return $this->definitionFields;
+  }
+
+  /**
+   * Defines the schema for the tree table.
+   *
+   * @return array
+   *   The schema API definition for the SQL storage 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.
+        //   https://www.drupal.org/node/2302197
+        '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 000000000000..a5d512d4ee5c
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php
@@ -0,0 +1,278 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\MenuTreeStorageInterface.
+ */
+
+namespace Drupal\Core\Menu;
+
+/**
+ * Defines an interface for storing a menu tree containing menu link IDs.
+ *
+ * The tree contains a hierarchy of menu links which have an ID as well as a
+ * route name or external URL.
+ */
+interface MenuTreeStorageInterface {
+
+  /**
+   * The maximum depth of tree the storage implementation supports.
+   *
+   * @return int
+   *   The maximum depth.
+   */
+  public function maxDepth();
+
+  /**
+   * 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.
+   *
+   */
+  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
+   *   The plugin definition, or FALSE if no definition was found for the ID.
+   */
+  public function load($id);
+
+  /**
+   * Loads multiple plugin definitions from the storage.
+   *
+   * @param array $ids
+   *   An array of plugin IDs.
+   *
+   * @return array
+   *   An array of plugin definition arrays keyed by plugin ID, which are the
+   *   actual definitions after the loadMultiple() including all those plugins
+   *   from $ids.
+   */
+  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 definition arrays.
+   */
+  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 menu names affected by the save operation. This will be one menu
+   *   name if the link is saved to the sane menu, or two if it is saved to a
+   *   new menu.
+   *
+   * @throws \Exception
+   *   Thrown if the storage back-end does not exist and could not be created.
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   Thrown 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 containing an array.
+   *     @see static::treeDataRecursive()
+   *   - 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
+   *   (optional) The maximum depth of child menu links relative to the passed
+   *   in. Defaults to NULL, in which case the full subtree will be returned.
+   *
+   * @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. An example $id (8) with
+   *   two parents (1, 6) looks like the following:
+   * @code
+   *   array(
+   *     'p1' => 1,
+   *     'p2' => 6,
+   *     'p3' => 8,
+   *     'p4' => 0,
+   *     'p5' => 0,
+   *     'p6' => 0,
+   *     'p7' => 0,
+   *     'p8' => 0,
+   *     'p9' => 0
+   *   )
+   * @endcode
+   */
+  public function getRootPathIds($id);
+
+  /**
+   * Finds 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 the given ID.
+   *
+   * @param string $id
+   *   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 name 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 all menus.
+   *
+   * @return int
+   *   The number of menu links in the named menu, or in all menus if the menu
+   *   name is NULL.
+   */
+  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 000000000000..8b79bffc253c
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/StaticMenuLinkOverrides.php
@@ -0,0 +1,163 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Menu\StaticMenuLinkOverrides.
+ */
+
+namespace Drupal\Core\Menu;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+
+/**
+ * Defines an 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;
+  }
+
+  /**
+   * Gets the configuration object when needed.
+   *
+   * 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) {
+    // Only allow to override a specific subset of the keys.
+    $expected = array(
+      'menu_name' => 1,
+      'parent' => 1,
+      'weight' => 1,
+      'expanded' => 1,
+      'hidden' => 1,
+    );
+    // Filter the overrides to only those that are expected.
+    $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. Double underscores are converted to triple underscores to
+   * avoid accidental conflicts.
+   *
+   * @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 strtr($id, array('.' => '__', '__' => '___'));
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php b/core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php
new file mode 100644
index 000000000000..0ffb21a0fb22
--- /dev/null
+++ b/core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php
@@ -0,0 +1,87 @@
+<?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 {
+
+  /**
+   * Reloads the overrides from config.
+   *
+   * Forces all overrides to be reloaded from config storage to compare the
+   * override value with the value submitted during test form submission.
+   */
+  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
+   *   or NULL if there is no override for the given ID.
+   */
+  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 5febcd3e66d3..9ab429f2ac17 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 8bfde35c6197..9efdcf2c22f1 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 000000000000..5ea5cc71bf2b
--- /dev/null
+++ b/core/modules/menu_link_content/menu_link_content.info.yml
@@ -0,0 +1,6 @@
+name: 'Custom Menu Links'
+type: module
+description: 'Allows administrators to create custom menu 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 000000000000..25dfb2692151
--- /dev/null
+++ b/core/modules/menu_link_content/menu_link_content.module
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @file
+ * Allows administrators to create custom menu links.
+ */
+
+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 000000000000..411adf126ae1
--- /dev/null
+++ b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
@@ -0,0 +1,385 @@
+<?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("Custom menu link"),
+ *   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 getPluginDefinition() {
+    $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();
+    $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->updateDefinition($this->getPluginId(), $this->getPluginDefinition(), FALSE);
+      }
+    }
+    else {
+      $menu_link_manager->addDefinition($this->getPluginId(), $this->getPluginDefinition());
+    }
+  }
+
+  /**
+   * {@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->removeDefinition($menu_link->getPluginId(), FALSE);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields['id'] = FieldDefinition::create('integer')
+      ->setLabel(t('Entity ID'))
+      ->setDescription(t('The entity ID for this menu link content entity.'))
+      ->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 https://www.drupal.org/node/2302205.
+    $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('uri')
+      ->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', FALSE);
+
+    // 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', FALSE)
+      ->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 hidden in menus or rendered normally.'))
+      ->setSetting('default_value', FALSE);
+
+    $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 plugin ID'))
+      ->setDescription(t('The ID of the parent menu link plugin, or empty string when at the top level of the hierarchy.'));
+
+    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 000000000000..d64bab9854e7
--- /dev/null
+++ b/core/modules/menu_link_content/src/Entity/MenuLinkContentInterface.php
@@ -0,0 +1,152 @@
+<?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 {
+
+  /**
+   * Flags 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 menu link.
+   *
+   * @return string|NULL
+   *   Returns the route name, or NULL if it is an external link.
+   */
+  public function getRouteName();
+
+  /**
+   * Gets the route parameters of the menu link content entity.
+   *
+   * @return array
+   *   The route parameters, or an empty array.
+   */
+  public function getRouteParameters();
+
+  /**
+   * Sets the route parameters of the custom menu link.
+   *
+   * @param array $route_parameters
+   *   The route parameters, usually derived from the path entered by the
+   *   administrator. For example, for a link to a node with route 'node.view'
+   *   the route needs the node ID as a parameter:
+   *   @code
+   *     array('node' => 2)
+   *   @endcode
+   *
+   * @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 menu link content entity.
+   *
+   * @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 menu link content entity.
+   *
+   * @return array
+   *   The options that may be passed to the URL generator.
+   */
+  public function getOptions();
+
+  /**
+   * Sets the query options of the menu link content entity.
+   *
+   * @param array $options
+   *   The new option.
+   *
+   * @return $this
+   */
+  public function setOptions(array $options);
+
+  /**
+   * Gets the description of the menu link for the UI.
+   *
+   * @return string
+   *   The description to 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 menu link content entity.
+   *
+   * @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 000000000000..c99109e40621
--- /dev/null
+++ b/core/modules/menu_link_content/src/MenuLinkContentAccessController.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\menu_link_content\MenuLinkContentAccessController.
+ */
+
+namespace Drupal\menu_link_content;
+
+use Drupal\Core\Access\AccessManager;
+use Drupal\Core\Entity\EntityControllerInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityAccessController;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines the access controller for the user entity type.
+ */
+class MenuLinkContentAccessController extends EntityAccessController implements EntityControllerInterface {
+
+  /**
+   * The access manager to check routes by name.
+   *
+   * @var \Drupal\Core\Access\AccessManager
+   */
+  protected $accessManager;
+
+  /**
+   * Creates a new MenuLinkContentAccessController.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type definition.
+   * @param \Drupal\Core\Access\AccessManager $access_manager
+   *   The access manager to check routes by name.
+   */
+  public function __construct(EntityTypeInterface $entity_type, AccessManager $access_manager) {
+    parent::__construct($entity_type);
+
+    $this->accessManager = $access_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static($entity_type, $container->get('access_manager'));
+  }
+
+  /**
+   * {@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');
+    }
+  }
+
+}
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 000000000000..a55dc9b168c2
--- /dev/null
+++ b/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php
@@ -0,0 +1,250 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_link_content\Plugin\Menu\MenuLinkContent.
+ */
+
+namespace Drupal\menu_link_content\Plugin\Menu;
+
+use Drupal\Component\Plugin\Exception\PluginException;
+use Drupal\Component\Utility\String;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\Core\Menu\MenuLinkBase;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides the menu link plugin for content menu links.
+ */
+class MenuLinkContent extends MenuLinkBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * Entities IDs to load.
+   *
+   * It is an array of entity IDs keyed by entity IDs.
+   *
+   * @var array
+   */
+  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'];
+      // Builds a list of entity IDs to take advantage of the more efficient
+      // EntityStorageInterface::loadMultiple() in getEntity() at render time.
+      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'];
+        // Make sure the current ID is in the list, since each plugin empties
+        // the list after calling loadMultple(). Note that the list may include
+        // multiple IDs added earlier in each plugin's constructor.
+        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();
+      }
+      if (!$entity) {
+        // Fallback to the loading by the UUID.
+        $uuid = $this->getDerivativeId();
+        $loaded_entities = $storage->loadByProperties(array('uuid' => $uuid));
+        $entity = reset($loaded_entities);
+      }
+      if (!$entity) {
+        throw new PluginException(String::format('Entity not found through the menu link plugin definition and could not fallback on UUID @uuid', array('@uuid' => $uuid)));
+      }
+      // Clone the entity object to avoid tampering with the static cache.
+      $this->entity = clone $entity;
+      $the_entity = $this->entityManager->getTranslationFromContext($this->entity);
+      /** @var \Drupal\menu_link_content\Entity\MenuLinkContentInterface $the_entity */
+      $this->entity = $the_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 is configured to be multilingual.
+    if ($this->langaugeManager->isMultilingual()) {
+      return $this->getEntity()->getTitle();
+    }
+    return $this->pluginDefinition['title'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    // We only need to get the description from the actual entity if it may be a
+    // translation based on the current language context. This can only happen
+    // if the site is configured to be multilingual.
+    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 = 'menu_link_content';
+    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) {
+    // Filter the list of updates to only those that are allowed.
+    $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() {
+    $this->getEntity()->delete();
+  }
+
+}
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 000000000000..75b4f1e225fc
--- /dev/null
+++ b/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php
@@ -0,0 +1,389 @@
+<?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.
+ *
+ * @group Menu
+ *
+ * @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}
+   */
+  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/StaticMenuLinkOverridesTest.php b/core/tests/Drupal/Tests/Core/Menu/StaticMenuLinkOverridesTest.php
new file mode 100644
index 000000000000..c436d6f391c1
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Menu/StaticMenuLinkOverridesTest.php
@@ -0,0 +1,219 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Menu\StaticMenuLinkOverridesTest.
+ */
+
+namespace Drupal\Tests\Core\Menu;
+
+use Drupal\Core\Menu\StaticMenuLinkOverrides;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Menu\StaticMenuLinkOverrides
+ * @group Menu
+ */
+class StaticMenuLinkOverridesTest extends UnitTestCase {
+
+  /**
+   * Tests the constructor.
+   *
+   * @covers ::__construct
+   */
+  public function testConstruct() {
+    $config_factory = $this->getConfigFactoryStub(array('menu_link.static.overrides' => array()));
+    $static_override = new StaticMenuLinkOverrides($config_factory);
+
+    $this->assertAttributeEquals($config_factory, 'configFactory', $static_override);
+  }
+
+  /**
+   * Tests the reload method.
+   *
+   * @covers ::reload
+   */
+  public function testReload() {
+    $config_factory = $this->getMock('Drupal\Core\Config\ConfigFactoryInterface');
+    $config_factory->expects($this->at(0))
+      ->method('reset')
+      ->with('menu_link.static.overrides');
+
+    $static_override = new StaticMenuLinkOverrides($config_factory);
+
+    $static_override->reload();
+  }
+
+  /**
+   * Tests the loadOverride method.
+   *
+   * @dataProvider providerTestLoadOverride
+   *
+   * @covers ::loadOverride
+   * @covers ::getConfig
+   */
+  public function testLoadOverride($overrides, $id, $expected) {
+    $config_factory = $this->getConfigFactoryStub(array('menu_link.static.overrides' => array('definitions' => $overrides)));
+    $static_override = new StaticMenuLinkOverrides($config_factory);
+
+    $this->assertEquals($expected, $static_override->loadOverride($id));
+  }
+
+  /**
+   * Provides test data for testLoadOverride.
+   */
+  public function providerTestLoadOverride() {
+    $data = array();
+    // No specified ID.
+    $data[] = array(array('test1' => array('parent' => 'test0')), NULL, array());
+    // Valid ID.
+    $data[] = array(array('test1' => array('parent' => 'test0')), 'test1', array('parent' => 'test0'));
+    // Non existing ID.
+    $data[] = array(array('test1' => array('parent' => 'test0')), 'test2', array());
+    // Ensure that the ID is encoded properly
+    $data[] = array(array('test1__la___ma' => array('parent' => 'test0')), 'test1.la__ma', array('parent' => 'test0'));
+
+    return $data;
+  }
+
+  /**
+   * Tests the loadMultipleOverrides method.
+   *
+   * @covers ::loadMultipleOverrides
+   * @covers ::getConfig
+   */
+  public function testLoadMultipleOverrides() {
+    $overrides = array();
+    $overrides['test1'] = array('parent' => 'test0');
+    $overrides['test2'] = array('parent' => 'test1');
+    $overrides['test1__la___ma'] = array('parent' => 'test2');
+
+    $config_factory = $this->getConfigFactoryStub(array('menu_link.static.overrides' => array('definitions' => $overrides)));
+    $static_override = new StaticMenuLinkOverrides($config_factory);
+
+    $this->assertEquals(array('test1' => array('parent' => 'test0'), 'test1.la__ma' => array('parent' => 'test2')), $static_override->loadMultipleOverrides(array('test1', 'test1.la__ma')));
+  }
+
+  /**
+   * Tests the saveOverride method.
+   *
+   * @covers ::saveOverride
+   * @covers ::loadOverride
+   * @covers ::getConfig
+   */
+  public function testSaveOverride() {
+    $config = $this->getMockBuilder('Drupal\Core\Config\Config')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $config->expects($this->at(0))
+      ->method('get')
+      ->with('definitions')
+      ->will($this->returnValue(array()));
+    $config->expects($this->at(1))
+      ->method('get')
+      ->with('definitions')
+      ->will($this->returnValue(array()));
+
+    $definition_save_1 = array('definitions' => array('test1' => array('parent' => 'test0')));
+    $definitions_save_2 = array(
+      'definitions' => array(
+        'test1' => array('parent' => 'test0'),
+        'test1__la___ma' => array('parent' => 'test1')
+      )
+    );
+    $config->expects($this->at(2))
+      ->method('set')
+      ->with('definitions', $definition_save_1['definitions'])
+      ->will($this->returnSelf());
+    $config->expects($this->at(3))
+      ->method('save');
+    $config->expects($this->at(4))
+      ->method('get')
+      ->with('definitions')
+      ->will($this->returnValue($definition_save_1['definitions']));
+    $config->expects($this->at(5))
+      ->method('get')
+      ->with('definitions')
+      ->will($this->returnValue($definition_save_1['definitions']));
+    $config->expects($this->at(6))
+      ->method('set')
+      ->with('definitions', $definitions_save_2['definitions'])
+      ->will($this->returnSelf());
+    $config->expects($this->at(7))
+      ->method('save');
+
+    $config_factory = $this->getMock('Drupal\Core\Config\ConfigFactoryInterface');
+    $config_factory->expects($this->once())
+      ->method('get')
+      ->will($this->returnValue($config));
+
+    $static_override = new StaticMenuLinkOverrides($config_factory);
+
+    $static_override->saveOverride('test1', array('parent' => 'test0'));
+    $static_override->saveOverride('test1.la__ma', array('parent' => 'test1'));
+  }
+
+  /**
+   * Tests the deleteOverride and deleteOverrides method.
+   *
+   * @param array|string $ids
+   *   Either a single ID or multiple ones as array.
+   * @param array $old_definitions
+   *   The definitions before the deleting
+   * @param array $new_definitions
+   *   The definitions after the deleting.
+   *
+   * @dataProvider providerTestDeleteOverrides
+   */
+  public function testDeleteOverrides($ids, array $old_definitions, array $new_definitions) {
+    $config = $this->getMockBuilder('Drupal\Core\Config\Config')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $config->expects($this->at(0))
+      ->method('get')
+      ->with('definitions')
+      ->will($this->returnValue($old_definitions));
+
+    // Only save if the definitions changes.
+    if ($old_definitions != $new_definitions) {
+      $config->expects($this->at(1))
+        ->method('set')
+        ->with('definitions', $new_definitions)
+        ->will($this->returnSelf());
+      $config->expects($this->at(2))
+        ->method('save');
+    }
+
+    $config_factory = $this->getMock('Drupal\Core\Config\ConfigFactoryInterface');
+    $config_factory->expects($this->once())
+      ->method('get')
+      ->will($this->returnValue($config));
+
+    $static_override = new StaticMenuLinkOverrides($config_factory);
+
+    if (is_array($ids)) {
+      $static_override->deleteMultipleOverrides($ids);
+    }
+    else {
+      $static_override->deleteOverride($ids);
+    }
+  }
+
+  /**
+   * Provides test data for testDeleteOverrides.
+   */
+  public function providerTestDeleteOverrides() {
+    $data = array();
+    // Delete a non existing ID.
+    $data[] = array('test0', array(), array());
+    // Delete an existing ID.
+    $data[] = array('test1', array('test1' => array('parent' => 'test0')), array());
+    // Delete an existing ID with a special ID.
+    $data[] = array('test1.la__ma', array('test1__la___ma' => array('parent' => 'test0')), array());
+    // Delete multiple IDs.
+    $data[] = array(array('test1.la__ma', 'test1'), array('test1' => array('parent' => 'test0'), 'test1__la___ma' => array('parent' => 'test0')), array());
+
+    return $data;
+  }
+
+}
-- 
GitLab