diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt
index 5969d4b..e1a2ff9 100644
--- a/core/MAINTAINERS.txt
+++ b/core/MAINTAINERS.txt
@@ -234,6 +234,10 @@ Locale module
 Menu module
 - ?
 
+Menu Link module
+- Andrei Mateescu 'amateescu' http://drupal.org/user/729614
+- @todo Anyone else from the menu system?
+
 Node module
 - Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23
 - David Strauss 'David Strauss' http://drupal.org/user/93254
diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index 6492b10..6d61f3a 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -5,8 +5,11 @@
  * API for the Drupal menu system.
  */
 
+use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Template\Attribute;
+use Drupal\menu_link\Plugin\Core\Entity\MenuLink;
+use Drupal\menu_link\MenuLinkStorageController;
 
 /**
  * @defgroup menu Menu system
@@ -264,6 +267,9 @@
 
 /**
  * The maximum depth of a menu links tree - matches the number of p columns.
+ *
+ * @todo Move this constant to MenuLinkStorageController along with all the tree
+ * functionality.
  */
 const MENU_MAX_DEPTH = 9;
 
@@ -1260,17 +1266,18 @@ function menu_tree_page_data($menu_name, $max_depth = NULL, $only_active_trail =
             // Collect all the links set to be expanded, and then add all of
             // their children to the list as well.
             do {
-              $result = db_select('menu_links', NULL, array('fetch' => PDO::FETCH_ASSOC))
-                ->fields('menu_links', array('mlid'))
+              $query = entity_query('menu_link')
                 ->condition('menu_name', $menu_name)
                 ->condition('expanded', 1)
                 ->condition('has_children', 1)
                 ->condition('plid', $parents, 'IN')
-                ->condition('mlid', $parents, 'NOT IN')
-                ->execute();
+                ->condition('mlid', $parents, 'NOT IN');
+              $result = $query->execute();
               $num_rows = FALSE;
-              foreach ($result as $item) {
-                $parents[$item['mlid']] = $item['mlid'];
+              if (!empty($result)) {
+                foreach ($result as $mlid) {
+                  $parents[$mlid] = $mlid;
+                }
                 $num_rows = TRUE;
               }
             } while ($num_rows);
@@ -1360,48 +1367,24 @@ function _menu_build_tree($menu_name, array $parameters = array()) {
   }
 
   if (!isset($trees[$tree_cid])) {
-    // Select the links from the table, and recursively build the tree. We
-    // LEFT JOIN since there is no match in {menu_router} for an external
-    // link.
-    $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC));
-    $query->addTag('translatable');
-    $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
-    $query->fields('ml');
-    $query->fields('m', array(
-      'load_functions',
-      'to_arg_functions',
-      'access_callback',
-      'access_arguments',
-      'page_callback',
-      'page_arguments',
-      'tab_parent',
-      'tab_root',
-      'title',
-      'title_callback',
-      'title_arguments',
-      'theme_callback',
-      'theme_arguments',
-      'type',
-      'description',
-      'description_callback',
-      'description_arguments',
-    ));
+    $links = array();
+    $query = entity_query('menu_link');
     for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) {
-      $query->orderBy('p' . $i, 'ASC');
+      $query->sort('p' . $i, 'ASC');
     }
-    $query->condition('ml.menu_name', $menu_name);
+    $query->condition('menu_name', $menu_name);
     if (!empty($parameters['expanded'])) {
-      $query->condition('ml.plid', $parameters['expanded'], 'IN');
+      $query->condition('plid', $parameters['expanded'], 'IN');
     }
     elseif (!empty($parameters['only_active_trail'])) {
-      $query->condition('ml.mlid', $parameters['active_trail'], 'IN');
+      $query->condition('mlid', $parameters['active_trail'], 'IN');
     }
     $min_depth = (isset($parameters['min_depth']) ? $parameters['min_depth'] : 1);
     if ($min_depth != 1) {
-      $query->condition('ml.depth', $min_depth, '>=');
+      $query->condition('depth', $min_depth, '>=');
     }
     if (isset($parameters['max_depth'])) {
-      $query->condition('ml.depth', $parameters['max_depth'], '<=');
+      $query->condition('depth', $parameters['max_depth'], '<=');
     }
     // Add custom query conditions, if any were passed.
     if (isset($parameters['conditions'])) {
@@ -1410,10 +1393,8 @@ function _menu_build_tree($menu_name, array $parameters = array()) {
       }
     }
 
-    // Build an ordered array of links using the query result object.
-    $links = array();
-    foreach ($query->execute() as $item) {
-      $links[] = $item;
+    if ($result = $query->execute()) {
+      $links = menu_link_load_multiple($result);
     }
     $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array());
     $data['tree'] = menu_tree_data($links, $active_trail, $min_depth);
@@ -2451,18 +2432,11 @@ function menu_link_get_preferred($path = NULL, $selected_menu = NULL) {
     // Put the selected menu at the front of the list.
     array_unshift($menu_names, $selected_menu);
 
-    $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC));
-    $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
-    $query->fields('ml');
-    // Weight must be taken from {menu_links}, not {menu_router}.
-    $query->addField('ml', 'weight', 'link_weight');
-    $query->fields('m');
-    $query->condition('ml.link_path', $path_candidates, 'IN');
+    $menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $path_candidates));
 
     // Sort candidates by link path and menu name.
     $candidates = array();
-    foreach ($query->execute() as $candidate) {
-      $candidate['weight'] = $candidate['link_weight'];
+    foreach ($menu_links as $candidate) {
       $candidates[$candidate['link_path']][$candidate['menu_name']] = $candidate;
       // Add any menus not already in the menu name search list.
       if (!in_array($candidate['menu_name'], $menu_names)) {
@@ -2582,38 +2556,6 @@ function menu_get_active_title() {
 }
 
 /**
- * Gets a translated, access-checked menu link that is ready for rendering.
- *
- * This function should never be called from within node_load() or any other
- * function used as a menu object load function since an infinite recursion may
- * occur.
- *
- * @param $mlid
- *   The mlid of the menu item.
- *
- * @return
- *   A menu link, with $item['access'] filled and link translated for
- *   rendering.
- */
-function menu_link_load($mlid) {
-  if (is_numeric($mlid)) {
-    $query = db_select('menu_links', 'ml');
-    $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
-    $query->fields('ml');
-    // Weight should be taken from {menu_links}, not {menu_router}.
-    $query->addField('ml', 'weight', 'link_weight');
-    $query->fields('m');
-    $query->condition('ml.mlid', $mlid);
-    if ($item = $query->execute()->fetchAssoc()) {
-      $item['weight'] = $item['link_weight'];
-      _menu_link_translate($item);
-      return $item;
-    }
-  }
-  return FALSE;
-}
-
-/**
  * Clears the cached cached data for a single named menu.
  */
 function menu_cache_clear($menu_name = 'tools') {
@@ -2637,6 +2579,7 @@ function menu_cache_clear_all() {
  * Resets the menu system static cache.
  */
 function menu_reset_static_cache() {
+  entity_get_controller('menu_link')->resetCache();
   drupal_static_reset('_menu_build_tree');
   drupal_static_reset('menu_tree');
   drupal_static_reset('menu_tree_all_data');
@@ -2735,156 +2678,106 @@ function menu_get_router() {
 }
 
 /**
- * Builds a link from a router item.
- */
-function _menu_link_build($item) {
-  // Suggested items are disabled by default.
-  if ($item['type'] == MENU_SUGGESTED_ITEM) {
-    $item['hidden'] = 1;
-  }
-  // Hide all items that are not visible in the tree.
-  elseif (!($item['type'] & MENU_VISIBLE_IN_TREE)) {
-    $item['hidden'] = -1;
-  }
-  // Note, we set this as 'system', so that we can be sure to distinguish all
-  // the menu links generated automatically from entries in {menu_router}.
-  $item['module'] = 'system';
-  $item += array(
-    'menu_name' => 'tools',
-    'link_title' => $item['title'],
-    'link_path' => $item['path'],
-    'hidden' => 0,
-    'options' => empty($item['description']) ? array() : array('attributes' => array('title' => $item['description'])),
-  );
-  return $item;
-}
-
-/**
  * Builds menu links for the items in the menu router.
  */
 function _menu_navigation_links_rebuild($menu) {
+  $menu_link_controller = entity_get_controller('menu_link');
+
   // Add normal and suggested items as links.
-  $menu_links = array();
-  foreach ($menu as $path => $item) {
-    if ($item['_visible']) {
-      $menu_links[$path] = $item;
-      $sort[$path] = $item['_number_parts'];
+  $router_items = array();
+  foreach ($menu as $path => $router_item) {
+    if ($router_item['_visible']) {
+      $router_items[$path] = $router_item;
+      $sort[$path] = $router_item['_number_parts'];
     }
   }
-  if ($menu_links) {
-    // Keep an array of processed menu links, to allow menu_link_save() to
-    // check this for parents instead of querying the database.
+  if ($router_items) {
+    // Keep an array of processed menu links, to allow
+    // Drupal\menu_link\MenuLinkStorageController::save() to check this for
+    // parents instead of querying the database.
     $parent_candidates = array();
     // Make sure no child comes before its parent.
-    array_multisort($sort, SORT_NUMERIC, $menu_links);
+    array_multisort($sort, SORT_NUMERIC, $router_items);
 
-    foreach ($menu_links as $key => $item) {
+    foreach ($router_items as $key => $router_item) {
+      // For performance reasons, do a straight query now and convert to a menu
+      // link entity later.
       $existing_item = db_select('menu_links')
-        ->fields('menu_links')
-        ->condition('link_path', $item['path'])
-        ->condition('module', 'system')
-        ->execute()->fetchAssoc();
+         ->fields('menu_links')
+         ->condition('link_path', $router_item['path'])
+         ->condition('module', 'system')
+         ->execute()->fetchAll();
       if ($existing_item) {
-        $item['mlid'] = $existing_item['mlid'];
+        $existing_item = reset($existing_item);
+        $existing_item->options = unserialize($existing_item->options);
+
+        $router_item['mlid'] = $existing_item->mlid;
+        $router_item['uuid'] = $existing_item->uuid;
         // A change in hook_menu may move the link to a different menu
-        if (empty($item['menu_name']) || ($item['menu_name'] == $existing_item['menu_name'])) {
-          $item['menu_name'] = $existing_item['menu_name'];
-          $item['plid'] = $existing_item['plid'];
+        if (empty($router_item['menu_name']) || ($router_item['menu_name'] == $existing_item->menu_name)) {
+          $router_item['menu_name'] = $existing_item->menu_name;
+          $router_item['plid'] = $existing_item->plid;
         }
         else {
-          // It moved to a new menu. Let menu_link_save() try to find a new
-          // parent based on the path.
-          unset($item['plid']);
+          // It moved to a new menu.
+          // Let Drupal\menu_link\MenuLinkStorageController::save() try to find
+          // a new parent based on the path.
+          unset($router_item['plid']);
         }
-        $item['has_children'] = $existing_item['has_children'];
-        $item['updated'] = $existing_item['updated'];
+        $router_item['has_children'] = $existing_item->has_children;
+        $router_item['updated'] = $existing_item->updated;
+
+        // Convert the existing item to a typed object.
+        $existing_item = $menu_link_controller->create(get_object_vars($existing_item));
       }
-      if ($existing_item && $existing_item['customized']) {
-        $parent_candidates[$existing_item['mlid']] = $existing_item;
+      else {
+        $existing_item = NULL;
+      }
+
+      if ($existing_item && $existing_item->customized) {
+        $parent_candidates[$existing_item->mlid] = $existing_item;
       }
       else {
-        $item = _menu_link_build($item);
-        menu_link_save($item, $existing_item, $parent_candidates);
-        $parent_candidates[$item['mlid']] = $item;
-        unset($menu_links[$key]);
+        $menu_link = MenuLink::buildFromRouterItem($router_item);
+        $menu_link->original = $existing_item;
+        $menu_link->parentCandidates = $parent_candidates;
+        $menu_link_controller->save($menu_link);
+        $parent_candidates[$menu_link->id()] = $menu_link;
+        unset($router_items[$key]);
       }
     }
   }
+
   $paths = array_keys($menu);
   // Updated and customized items whose router paths are gone need new ones.
-  $result = db_select('menu_links', NULL, array('fetch' => PDO::FETCH_ASSOC))
-    ->fields('menu_links', array(
-      'link_path',
-      'mlid',
-      'router_path',
-      'updated',
-    ))
-    ->condition(db_or()
-      ->condition('updated', 1)
-      ->condition(db_and()
-        ->condition('router_path', $paths, 'NOT IN')
-        ->condition('external', 0)
-        ->condition('customized', 1)
-      )
-    )
-    ->execute();
-  foreach ($result as $item) {
-    $router_path = _menu_find_router_path($item['link_path']);
-    if (!empty($router_path) && ($router_path != $item['router_path'] || $item['updated'])) {
+  $menu_links = entity_get_controller('menu_link')->loadUpdatedCustomized($paths);
+  foreach ($menu_links as $menu_link) {
+    $router_path = _menu_find_router_path($menu_link->link_path);
+    if (!empty($router_path) && ($router_path != $menu_link->router_path || $menu_link->updated)) {
       // If the router path and the link path matches, it's surely a working
       // item, so we clear the updated flag.
-      $updated = $item['updated'] && $router_path != $item['link_path'];
-      db_update('menu_links')
-        ->fields(array(
-          'router_path' => $router_path,
-          'updated' => (int) $updated,
-        ))
-        ->condition('mlid', $item['mlid'])
-        ->execute();
+      $updated = $menu_link->updated && $router_path != $menu_link->link_path;
+
+      $menu_link->router_path = $router_path;
+      $menu_link->updated = (int) $updated;
+      $menu_link_controller->save($menu_link);
     }
   }
+
   // Find any item whose router path does not exist any more.
-  $result = db_select('menu_links')
-    ->fields('menu_links')
+  $query = entity_query('menu_link')
     ->condition('router_path', $paths, 'NOT IN')
     ->condition('external', 0)
     ->condition('updated', 0)
     ->condition('customized', 0)
-    ->orderBy('depth', 'DESC')
-    ->execute();
-  // Remove all such items. Starting from those with the greatest depth will
-  // minimize the amount of re-parenting done by menu_link_delete().
-  foreach ($result as $item) {
-    _menu_delete_item($item, TRUE);
-  }
-}
+    ->sort('depth', 'DESC');
+  $result = $query->execute();
 
-/**
- * Clones an array of menu links.
- *
- * @param $links
- *   An array of menu links to clone.
- * @param $menu_name
- *   (optional) The name of a menu that the links will be cloned for. If not
- *   set, the cloned links will be in the same menu as the original set of
- *   links that were passed in.
- *
- * @return
- *   An array of menu links with the same properties as the passed-in array,
- *   but with the link identifiers removed so that a new link will be created
- *   when any of them is passed in to menu_link_save().
- *
- * @see menu_link_save()
- */
-function menu_links_clone($links, $menu_name = NULL) {
-  foreach ($links as &$link) {
-    unset($link['mlid']);
-    unset($link['plid']);
-    if (isset($menu_name)) {
-      $link['menu_name'] = $menu_name;
-    }
+  // Remove all such items. Starting from those with the greatest depth will
+  // minimize the amount of re-parenting done by the menu link controller.
+  if (!empty($result)) {
+    menu_link_delete_multiple($result, TRUE);
   }
-  return $links;
 }
 
 /**
@@ -2897,18 +2790,19 @@ function menu_links_clone($links, $menu_name = NULL) {
  *   An array of menu links.
  */
 function menu_load_links($menu_name) {
-  $links = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC))
-    ->fields('ml')
-    ->condition('ml.menu_name', $menu_name)
+  $links = array();
+
+  $query = entity_query('menu_link')
+    ->condition('menu_name', $menu_name)
     // Order by weight so as to be helpful for menus that are only one level
     // deep.
-    ->orderBy('weight')
-    ->execute()
-    ->fetchAll();
+    ->sort('weight');
+  $result = $query->execute();
 
-  foreach ($links as &$link) {
-    $link['options'] = unserialize($link['options']);
+  if (!empty($result)) {
+    $links = menu_link_load_multiple($result);
   }
+
   return $links;
 }
 
@@ -2920,333 +2814,7 @@ function menu_load_links($menu_name) {
  */
 function menu_delete_links($menu_name) {
   $links = menu_load_links($menu_name);
-  foreach ($links as $link) {
-    // To speed up the deletion process, we reset some link properties that
-    // would trigger re-parenting logic in _menu_delete_item() and
-    // _menu_update_parental_status().
-    $link['has_children'] = FALSE;
-    $link['plid'] = 0;
-    _menu_delete_item($link);
-  }
-}
-
-/**
- * Delete one or several menu links.
- *
- * @param $mlid
- *   A valid menu link mlid or NULL. If NULL, $path is used.
- * @param $path
- *   The path to the menu items to be deleted. $mlid must be NULL.
- */
-function menu_link_delete($mlid, $path = NULL) {
-  if (isset($mlid)) {
-    _menu_delete_item(db_query("SELECT * FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $mlid))->fetchAssoc());
-  }
-  else {
-    $result = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path", array(':link_path' => $path));
-    foreach ($result as $link) {
-      _menu_delete_item($link);
-    }
-  }
-}
-
-/**
- * Deletes a single menu link.
- *
- * @param $item
- *   Item to be deleted.
- * @param $force
- *   Forces deletion. Internal use only, setting to TRUE is discouraged.
- *
- * @see menu_link_delete()
- */
-function _menu_delete_item($item, $force = FALSE) {
-  $item = is_object($item) ? get_object_vars($item) : $item;
-  if ($item && ($item['module'] != 'system' || $item['updated'] || $force)) {
-    // Children get re-attached to the item's parent.
-    if ($item['has_children']) {
-      $result = db_query("SELECT mlid FROM {menu_links} WHERE plid = :plid", array(':plid' => $item['mlid']));
-      foreach ($result as $m) {
-        $child = menu_link_load($m->mlid);
-        $child['plid'] = $item['plid'];
-        menu_link_save($child);
-      }
-    }
-
-    // Notify modules we are deleting the item.
-    module_invoke_all('menu_link_delete', $item);
-
-    db_delete('menu_links')->condition('mlid', $item['mlid'])->execute();
-
-    // Update the has_children status of the parent.
-    _menu_update_parental_status($item);
-    menu_cache_clear($item['menu_name']);
-    _menu_clear_page_cache();
-  }
-}
-
-/**
- * Saves a menu link.
- *
- * After calling this function, rebuild the menu cache using
- * menu_cache_clear_all().
- *
- * @param $item
- *   An associative array representing a menu link item, with elements:
- *   - link_path: (required) The path of the menu item, which should be
- *     normalized first by calling drupal_container()->get('path.alias_manager')->getSystemPath() on it.
- *   - link_title: (required) Title to appear in menu for the link.
- *   - menu_name: (optional) The machine name of the menu for the link.
- *     Defaults to 'tools'.
- *   - weight: (optional) Integer to determine position in menu. Default is 0.
- *   - expanded: (optional) Boolean that determines if the item is expanded.
- *   - options: (optional) An array of options, see l() for more.
- *   - mlid: (optional) Menu link identifier, the primary integer key for each
- *     menu link. Can be set to an existing value, or to 0 or NULL
- *     to insert a new link.
- *   - plid: (optional) The mlid of the parent.
- *   - router_path: (optional) The path of the relevant router item.
- * @param $existing_item
- *   Optional, the current record from the {menu_links} table as an array.
- * @param $parent_candidates
- *   Optional array of menu links keyed by mlid. Used by
- *   _menu_navigation_links_rebuild() only.
- *
- * @return
- *   The mlid of the saved menu link, or FALSE if the menu link could not be
- *   saved.
- */
-function menu_link_save(&$item, $existing_item = array(), $parent_candidates = array()) {
-  drupal_alter('menu_link', $item);
-
-  // This is the easiest way to handle the unique internal path '<front>',
-  // since a path marked as external does not need to match a router path.
-  $item['external'] = (url_is_external($item['link_path'])  || $item['link_path'] == '<front>') ? 1 : 0;
-  // Load defaults.
-  $item += array(
-    'menu_name' => 'tools',
-    'weight' => 0,
-    'link_title' => '',
-    'hidden' => 0,
-    'has_children' => 0,
-    'expanded' => 0,
-    'options' => array(),
-    'module' => 'menu',
-    'customized' => 0,
-    'updated' => 0,
-  );
-  if (isset($item['mlid'])) {
-    if (!$existing_item) {
-      $existing_item = db_query('SELECT * FROM {menu_links} WHERE mlid = :mlid', array('mlid' => $item['mlid']))->fetchAssoc();
-    }
-    if ($existing_item) {
-      $existing_item['options'] = unserialize($existing_item['options']);
-    }
-  }
-  else {
-    $existing_item = FALSE;
-  }
-
-  // Try to find a parent link. If found, assign it and derive its menu.
-  $parent = _menu_link_find_parent($item, $parent_candidates);
-  if (!empty($parent['mlid'])) {
-    $item['plid'] = $parent['mlid'];
-    $item['menu_name'] = $parent['menu_name'];
-  }
-  // If no corresponding parent link was found, move the link to the top-level.
-  else {
-    $item['plid'] = 0;
-  }
-  $menu_name = $item['menu_name'];
-
-  if (!$existing_item) {
-    $item['mlid'] = db_insert('menu_links')
-      ->fields(array(
-        'menu_name' => $item['menu_name'],
-        'plid' => $item['plid'],
-        'link_path' => $item['link_path'],
-        'hidden' => $item['hidden'],
-        'external' => $item['external'],
-        'has_children' => $item['has_children'],
-        'expanded' => $item['expanded'],
-        'weight' => $item['weight'],
-        'module' => $item['module'],
-        'link_title' => $item['link_title'],
-        'options' => serialize($item['options']),
-        'customized' => $item['customized'],
-        'updated' => $item['updated'],
-      ))
-      ->execute();
-  }
-
-  // Directly fill parents for top-level links.
-  if ($item['plid'] == 0) {
-    $item['p1'] = $item['mlid'];
-    for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) {
-      $item["p$i"] = 0;
-    }
-    $item['depth'] = 1;
-  }
-  // Otherwise, ensure that this link's depth is not beyond the maximum depth
-  // and fill parents based on the parent link.
-  else {
-    if ($item['has_children'] && $existing_item) {
-      $limit = MENU_MAX_DEPTH - menu_link_children_relative_depth($existing_item) - 1;
-    }
-    else {
-      $limit = MENU_MAX_DEPTH - 1;
-    }
-    if ($parent['depth'] > $limit) {
-      return FALSE;
-    }
-    $item['depth'] = $parent['depth'] + 1;
-    _menu_link_parents_set($item, $parent);
-  }
-  // Need to check both plid and menu_name, since plid can be 0 in any menu.
-  if ($existing_item && ($item['plid'] != $existing_item['plid'] || $menu_name != $existing_item['menu_name'])) {
-    _menu_link_move_children($item, $existing_item);
-  }
-  // Find the router_path.
-  if (empty($item['router_path'])  || !$existing_item || ($existing_item['link_path'] != $item['link_path'])) {
-    if ($item['external']) {
-      $item['router_path'] = '';
-    }
-    else {
-      // Find the router path which will serve this path.
-      $item['parts'] = explode('/', $item['link_path'], MENU_MAX_PARTS);
-      $item['router_path'] = _menu_find_router_path($item['link_path']);
-    }
-  }
-  // If every value in $existing_item is the same in the $item, there is no
-  // reason to run the update queries or clear the caches. We use
-  // array_intersect_key() with the $item as the first parameter because
-  // $item may have additional keys left over from building a router entry.
-  // The intersect removes the extra keys, allowing a meaningful comparison.
-  if (!$existing_item || (array_intersect_key($item, $existing_item) != $existing_item)) {
-    db_update('menu_links')
-      ->fields(array(
-        'menu_name' => $item['menu_name'],
-        'plid' => $item['plid'],
-        'link_path' => $item['link_path'],
-        'router_path' => $item['router_path'],
-        'hidden' => $item['hidden'],
-        'external' => $item['external'],
-        'has_children' => $item['has_children'],
-        'expanded' => $item['expanded'],
-        'weight' => $item['weight'],
-        'depth' => $item['depth'],
-        'p1' => $item['p1'],
-        'p2' => $item['p2'],
-        'p3' => $item['p3'],
-        'p4' => $item['p4'],
-        'p5' => $item['p5'],
-        'p6' => $item['p6'],
-        'p7' => $item['p7'],
-        'p8' => $item['p8'],
-        'p9' => $item['p9'],
-        'module' => $item['module'],
-        'link_title' => $item['link_title'],
-        'options' => serialize($item['options']),
-        'customized' => $item['customized'],
-      ))
-      ->condition('mlid', $item['mlid'])
-      ->execute();
-    // Check the has_children status of the parent.
-    _menu_update_parental_status($item);
-    menu_cache_clear($menu_name);
-    if ($existing_item && $menu_name != $existing_item['menu_name']) {
-      menu_cache_clear($existing_item['menu_name']);
-    }
-    // Notify modules we have acted on a menu item.
-    $hook = 'menu_link_insert';
-    if ($existing_item) {
-      $hook = 'menu_link_update';
-    }
-    module_invoke_all($hook, $item);
-    // Now clear the cache.
-    _menu_clear_page_cache();
-  }
-  return $item['mlid'];
-}
-
-/**
- * Finds a possible parent for a given menu link.
- *
- * Because the parent of a given link might not exist anymore in the database,
- * we apply a set of heuristics to determine a proper parent:
- *
- *  - use the passed parent link if specified and existing.
- *  - else, use the first existing link down the previous link hierarchy
- *  - else, for system menu links (derived from hook_menu()), reparent
- *    based on the path hierarchy.
- *
- * @param $menu_link
- *   A menu link.
- * @param $parent_candidates
- *   An array of menu links keyed by mlid.
- *
- * @return
- *   A menu link structure of the possible parent or FALSE if no valid parent
- *   has been found.
- */
-function _menu_link_find_parent($menu_link, $parent_candidates = array()) {
-  $parent = FALSE;
-
-  // This item is explicitely top-level, skip the rest of the parenting.
-  if (isset($menu_link['plid']) && empty($menu_link['plid'])) {
-    return $parent;
-  }
-
-  // If we have a parent link ID, try to use that.
-  $candidates = array();
-  if (isset($menu_link['plid'])) {
-    $candidates[] = $menu_link['plid'];
-  }
-
-  // Else, if we have a link hierarchy try to find a valid parent in there.
-  if (!empty($menu_link['depth']) && $menu_link['depth'] > 1) {
-    for ($depth = $menu_link['depth'] - 1; $depth >= 1; $depth--) {
-      $candidates[] = $menu_link['p' . $depth];
-    }
-  }
-
-  foreach ($candidates as $mlid) {
-    if (isset($parent_candidates[$mlid])) {
-      $parent = $parent_candidates[$mlid];
-    }
-    else {
-      $parent = db_query("SELECT * FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $mlid))->fetchAssoc();
-    }
-    if ($parent) {
-      return $parent;
-    }
-  }
-
-  // If everything else failed, try to derive the parent from the path
-  // hierarchy. This only makes sense for links derived from menu router
-  // items (ie. from hook_menu()).
-  if ($menu_link['module'] == 'system') {
-    $query = db_select('menu_links');
-    $query->condition('module', 'system');
-    // We always respect the link's 'menu_name'; inheritance for router items is
-    // ensured in _menu_router_build().
-    $query->condition('menu_name', $menu_link['menu_name']);
-
-    // Find the parent - it must be unique.
-    $parent_path = $menu_link['link_path'];
-    do {
-      $parent = FALSE;
-      $parent_path = substr($parent_path, 0, strrpos($parent_path, '/'));
-      $new_query = clone $query;
-      $new_query->condition('link_path', $parent_path);
-      // Only valid if we get a unique result.
-      if ($new_query->countQuery()->execute()->fetchField() == 1) {
-        $parent = $new_query->fields('menu_links')->execute()->fetchAssoc();
-      }
-    } while ($parent === FALSE && $parent_path);
-  }
-
-  return $parent;
+  menu_link_delete_multiple(array_keys($links), FALSE, TRUE);
 }
 
 /**
@@ -3322,188 +2890,6 @@ function _menu_find_router_path($link_path) {
 }
 
 /**
- * Inserts, updates, enables, disables, or deletes an uncustomized menu link.
- *
- * @param string $module
- *   The name of the module that owns the link.
- * @param string $op
- *   Operation to perform: insert, update, enable, disable, or delete.
- * @param string $link_path
- *   The path this link points to.
- * @param string $link_title
- *   (optional) Title of the link to insert or new title to update the link to.
- *   Unused for delete.
- *
- * @return integer|null
- *   The insert op returns the mlid of the new item. Others op return NULL.
- */
-function menu_link_maintain($module, $op, $link_path, $link_title = NULL) {
-  switch ($op) {
-    case 'insert':
-      $menu_link = array(
-        'link_title' => $link_title,
-        'link_path' => $link_path,
-        'module' => $module,
-      );
-      return menu_link_save($menu_link);
-
-    case 'update':
-      $result = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path AND module = :module AND customized = 0", array(':link_path' => $link_path, ':module' => $module))->fetchAll(PDO::FETCH_ASSOC);
-      foreach ($result as $link) {
-        $existing = $link;
-        if (isset($link_title)) {
-          $link['link_title'] = $link_title;
-        }
-        $link['options'] = unserialize($link['options']);
-        menu_link_save($link, $existing);
-      }
-      break;
-
-    case 'enable':
-    case 'disable':
-      $result = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path AND module = :module AND customized = 0", array(':link_path' => $link_path, ':module' => $module))->fetchAll(PDO::FETCH_ASSOC);
-      foreach ($result as $link) {
-        $existing = $link;
-        $link['hidden'] = ($op == 'disable' ? 1 : 0);
-        $link['customized'] = 1;
-        if (isset($link_title)) {
-          $link['link_title'] = $link_title;
-        }
-        $link['options'] = unserialize($link['options']);
-        menu_link_save($link, $existing);
-      }
-      break;
-
-    case 'delete':
-      menu_link_delete(NULL, $link_path);
-      break;
-  }
-}
-
-/**
- * Finds the depth of an item's children relative to its depth.
- *
- * For example, if the item has a depth of 2, and the maximum of any child in
- * the menu link tree is 5, the relative depth is 3.
- *
- * @param $item
- *   An array representing a menu link item.
- *
- * @return
- *   The relative depth, or zero.
- *
- */
-function menu_link_children_relative_depth($item) {
-  $query = db_select('menu_links');
-  $query->addField('menu_links', 'depth');
-  $query->condition('menu_name', $item['menu_name']);
-  $query->orderBy('depth', 'DESC');
-  $query->range(0, 1);
-
-  $i = 1;
-  $p = 'p1';
-  while ($i <= MENU_MAX_DEPTH && $item[$p]) {
-    $query->condition($p, $item[$p]);
-    $p = 'p' . ++$i;
-  }
-
-  $max_depth = $query->execute()->fetchField();
-
-  return ($max_depth > $item['depth']) ? $max_depth - $item['depth'] : 0;
-}
-
-/**
- * Updates the children of a menu link that is being moved.
- *
- * The menu name, parents (p1 - p6), and depth are updated for all children of
- * the link, and the has_children status of the previous parent is updated.
- */
-function _menu_link_move_children($item, $existing_item) {
-  $query = db_update('menu_links');
-
-  $query->fields(array('menu_name' => $item['menu_name']));
-
-  $p = 'p1';
-  $expressions = array();
-  for ($i = 1; $i <= $item['depth']; $p = 'p' . ++$i) {
-    $expressions[] = array($p, ":p_$i", array(":p_$i" => $item[$p]));
-  }
-  $j = $existing_item['depth'] + 1;
-  while ($i <= MENU_MAX_DEPTH && $j <= MENU_MAX_DEPTH) {
-    $expressions[] = array('p' . $i++, 'p' . $j++, array());
-  }
-  while ($i <= MENU_MAX_DEPTH) {
-    $expressions[] = array('p' . $i++, 0, array());
-  }
-
-  $shift = $item['depth'] - $existing_item['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', $existing_item['menu_name']);
-  $p = 'p1';
-  for ($i = 1; $i <= MENU_MAX_DEPTH && $existing_item[$p]; $p = 'p' . ++$i) {
-    $query->condition($p, $existing_item[$p]);
-  }
-
-  $query->execute();
-
-  // Check the has_children status of the parent, while excluding this item.
-  _menu_update_parental_status($existing_item, TRUE);
-}
-
-/**
- * Checks and updates the 'has_children' status for the parent of a link.
- */
-function _menu_update_parental_status($item, $exclude = FALSE) {
-  // If plid == 0, there is nothing to update.
-  if ($item['plid']) {
-    // Check if at least one visible child exists in the table.
-    $query = db_select('menu_links');
-    $query->addField('menu_links', 'mlid');
-    $query->condition('menu_name', $item['menu_name']);
-    $query->condition('hidden', 0);
-    $query->condition('plid', $item['plid']);
-    $query->range(0, 1);
-    if ($exclude) {
-      $query->condition('mlid', $item['mlid'], '<>');
-    }
-    $parent_has_children = ((bool) $query->execute()->fetchField()) ? 1 : 0;
-    db_update('menu_links')
-      ->fields(array('has_children' => $parent_has_children))
-      ->condition('mlid', $item['plid'])
-      ->execute();
-  }
-}
-
-/**
- * Sets the p1 through p9 values for a menu link being saved.
- */
-function _menu_link_parents_set(&$item, $parent) {
-  $i = 1;
-  while ($i < $item['depth']) {
-    $p = 'p' . $i++;
-    $item[$p] = $parent[$p];
-  }
-  $p = 'p' . $i++;
-  // The parent (p1 - p9) corresponding to the depth always equals the mlid.
-  $item[$p] = $item['mlid'];
-  while ($i <= MENU_MAX_DEPTH) {
-    $p = 'p' . $i++;
-    $item[$p] = 0;
-  }
-}
-
-/**
  * Builds the router table based on the data from hook_menu().
  */
 function _menu_router_build($callbacks) {
diff --git a/core/modules/aggregator/aggregator.module b/core/modules/aggregator/aggregator.module
index 047026a..4c85673 100644
--- a/core/modules/aggregator/aggregator.module
+++ b/core/modules/aggregator/aggregator.module
@@ -482,7 +482,7 @@ function aggregator_save_category($edit) {
     $op = 'insert';
   }
   if (isset($op)) {
-    menu_link_maintain('aggregator', $op, $link_path, $edit['title']);
+    module_invoke('menu_link', 'maintain', 'aggregator', $op, $link_path, $edit['title']);
   }
 }
 
diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Tests/CategorizeFeedItemTest.php b/core/modules/aggregator/lib/Drupal/aggregator/Tests/CategorizeFeedItemTest.php
index ae71799..a8ac6c5 100644
--- a/core/modules/aggregator/lib/Drupal/aggregator/Tests/CategorizeFeedItemTest.php
+++ b/core/modules/aggregator/lib/Drupal/aggregator/Tests/CategorizeFeedItemTest.php
@@ -32,8 +32,8 @@ function testCategorizeFeedItem() {
     $this->assertTrue(!empty($category), 'The category found in database.');
 
     $link_path = 'aggregator/categories/' . $category->cid;
-    $menu_link = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path", array(':link_path' => $link_path))->fetch();
-    $this->assertTrue(!empty($menu_link), 'The menu link associated with the category found in database.');
+    $menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $link_path));
+    $this->assertTrue(!empty($menu_links), 'The menu link associated with the category found in database.');
 
     $feed = $this->createFeed();
     db_insert('aggregator_category_feed')
diff --git a/core/modules/book/book.info b/core/modules/book/book.info
index b42a72b..d7fa3c9 100644
--- a/core/modules/book/book.info
+++ b/core/modules/book/book.info
@@ -3,5 +3,6 @@ description = Allows users to create and organize related content in an outline.
 package = Core
 version = VERSION
 core = 8.x
+dependencies[] = menu_link
 dependencies[] = node
 configure = admin/content/book/settings
diff --git a/core/modules/book/book.module b/core/modules/book/book.module
index 4e15fb3..0b96189 100644
--- a/core/modules/book/book.module
+++ b/core/modules/book/book.module
@@ -5,8 +5,9 @@
  * Allows users to create and organize related content in an outline.
  */
 
-use Drupal\node\Plugin\Core\Entity\Node;
 use Drupal\Core\Template\Attribute;
+use Drupal\menu_link\MenuLinkStorageController;
+use Drupal\node\Plugin\Core\Entity\Node;
 
 /**
  * Implements hook_help().
@@ -641,7 +642,9 @@ function _book_update_outline(Node $node) {
     }
   }
 
-  if (menu_link_save($node->book)) {
+  $node->book = entity_create('menu_link', $node->book);
+  try {
+    menu_link_save($node->book);
     if ($new) {
       // Insert new.
       db_insert('book')
@@ -667,9 +670,10 @@ function _book_update_outline(Node $node) {
 
     return TRUE;
   }
-
-  // Failed to save the menu link.
-  return FALSE;
+  catch (Exception $e) {
+    // Failed to save the menu link.
+    return FALSE;
+  }
 }
 
 /**
@@ -1009,7 +1013,7 @@ function book_node_prepare(Node $node) {
  *   The depth limit for items in the parent select.
  */
 function _book_parent_depth_limit($book_link) {
-  return MENU_MAX_DEPTH - 1 - (($book_link['mlid'] && $book_link['has_children']) ? menu_link_children_relative_depth($book_link) : 0);
+  return MENU_MAX_DEPTH - 1 - (($book_link['mlid'] && $book_link['has_children']) ? entity_get_controller('menu_link')->findChildrenRelativeDepth($book_link) : 0);
 }
 
 /**
diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuNodeTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuNodeTest.php
index d2d023b..496acc4 100644
--- a/core/modules/menu/lib/Drupal/menu/Tests/MenuNodeTest.php
+++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuNodeTest.php
@@ -106,12 +106,12 @@ function testMenuNodeFormWidget() {
     $this->assertNoLink($node_title);
 
     // Add a menu link to the Administration menu.
-    $item = array(
+    $item = entity_create('menu_link', array(
       'link_path' => 'node/' . $node->nid,
       'link_title' => $this->randomName(16),
       'menu_name' => 'admin',
-    );
-    menu_link_save($item);
+    ));
+    $item->save();
 
     // Assert that disabled Administration menu is not shown on the
     // node/$nid/edit page.
@@ -128,12 +128,12 @@ function testMenuNodeFormWidget() {
     // Create a second node.
     $child_node = $this->drupalCreateNode(array('type' => 'article'));
     // Assign a menu link to the second node, being a child of the first one.
-    $child_item = array(
+    $child_item = entity_create('menu_link', array(
       'link_path' => 'node/'. $child_node->nid,
       'link_title' => $this->randomName(16),
       'plid' => $item['mlid'],
-    );
-    menu_link_save($child_item);
+    ));
+    $child_item->save();
     // Edit the first node.
     $this->drupalGet('node/'. $node->nid .'/edit');
     // Assert that it is not possible to set the parent of the first node to itself or the second node.
diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
index e0b9b92..79536d9 100644
--- a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
+++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php
@@ -200,7 +200,7 @@ function deleteCustomMenu($menu) {
     $this->assertRaw(t('The custom menu %title has been deleted.', array('%title' => $title)), 'Custom menu was deleted');
     $this->assertFalse(menu_load($menu_name), 'Custom menu was deleted');
     // Test if all menu links associated to the menu were removed from database.
-    $result = db_query("SELECT menu_name FROM {menu_links} WHERE menu_name = :menu_name", array(':menu_name' => $menu_name))->fetchField();
+    $result = entity_load_multiple_by_properties('menu_link', array('menu_name' => $menu_name));
     $this->assertFalse($result, 'All menu links associated to the custom menu were deleted.');
   }
 
@@ -302,7 +302,9 @@ function testMenuQueryAndFragment() {
    * @param string $link Link path.
    * @param string $menu_name Menu name.
    * @param string $weight Menu weight
-   * @return array Menu link created.
+   *
+   * @return \Drupal\menu_link\Plugin\Core\Entity\MenuLink $menu_link
+   *   A menu link entity.
    */
   function addMenuLink($plid = 0, $link = '<front>', $menu_name = 'tools', $expanded = TRUE, $weight = '0') {
     // View add menu link page.
@@ -323,14 +325,14 @@ function addMenuLink($plid = 0, $link = '<front>', $menu_name = 'tools', $expand
     // Add menu link.
     $this->drupalPost(NULL, $edit, t('Save'));
     $this->assertResponse(200);
-    // Unlike most other modules, there is no confirmation message displayed.
-    $this->assertText($title, 'Menu link was added');
+    $this->assertText('The menu link has been saved.');
 
-    $item = db_query('SELECT * FROM {menu_links} WHERE link_title = :title', array(':title' => $title))->fetchAssoc();
-    $this->assertTrue(t('Menu link was found in database.'));
-    $this->assertMenuLink($item['mlid'], array('menu_name' => $menu_name, 'link_path' => $link, 'has_children' => 0, 'plid' => $plid));
+    $menu_links = entity_load_multiple_by_properties('menu_link', array('link_title' => $title));
+    $menu_link = reset($menu_links);
+    $this->assertTrue('Menu link was found in database.');
+    $this->assertMenuLink($menu_link->id(), array('menu_name' => $menu_name, 'link_path' => $link, 'has_children' => 0, 'plid' => $plid));
 
-    return $item;
+    return $menu_link;
   }
 
   /**
@@ -413,11 +415,7 @@ function modifyMenuLink(&$item) {
     $edit['link_title'] = $title;
     $this->drupalPost("admin/structure/menu/item/$mlid/edit", $edit, t('Save'));
     $this->assertResponse(200);
-    // Unlike most other modules, there is no confirmation message displayed.
-
-    // Verify menu link.
-    $this->drupalGet('admin/structure/menu/manage/' . $item['menu_name']);
-    $this->assertText($title, 'Menu link was edited');
+    $this->assertText('The menu link has been saved.');
   }
 
   /**
@@ -521,8 +519,8 @@ function enableMenuLink($item) {
    */
   function assertMenuLink($mlid, array $expected_item) {
     // Retrieve menu link.
-    $item = db_query('SELECT * FROM {menu_links} WHERE mlid = :mlid', array(':mlid' => $mlid))->fetchAssoc();
-    $options = unserialize($item['options']);
+    $item = menu_link_load($mlid);
+    $options = $item->options;
     if (!empty($options['query'])) {
       $item['link_path'] .= '?' . drupal_http_build_query($options['query']);
     }
@@ -538,8 +536,16 @@ function assertMenuLink($mlid, array $expected_item) {
    * Get standard menu link.
    */
   private function getStandardMenuLink() {
+    $mlid = 0;
     // Retrieve menu link id of the Log out menu link, which will always be on the front page.
-    $mlid = db_query("SELECT mlid FROM {menu_links} WHERE module = 'system' AND router_path = 'user/logout'")->fetchField();
+    $query = entity_query('menu_link')
+      ->condition('module', 'system')
+      ->condition('router_path', 'user/logout');
+    $result = $query->execute();
+    if (!empty($result)) {
+      $mlid = reset($result);
+    }
+
     $this->assertTrue($mlid > 0, 'Standard menu link id was found');
     // Load menu link.
     // Use api function so that link is translated for rendering.
diff --git a/core/modules/menu/menu.admin.inc b/core/modules/menu/menu.admin.inc
index 4a78495..9a7b2ba 100644
--- a/core/modules/menu/menu.admin.inc
+++ b/core/modules/menu/menu.admin.inc
@@ -6,6 +6,7 @@
  */
 
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+use Drupal\menu_link\Plugin\Core\Entity\MenuLink;
 
 /**
  * Menu callback which shows an overview page of all the custom menus and their descriptions.
@@ -68,16 +69,18 @@ function theme_menu_admin_overview($variables) {
 function menu_overview_form($form, &$form_state, $menu) {
   global $menu_admin;
   $form['#attached']['css'] = array(drupal_get_path('module', 'menu') . '/menu.admin.css');
-  $sql = "
-    SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.title, m.title_callback, m.title_arguments, m.type, m.description, m.description_callback, m.description_arguments, ml.*
-    FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
-    WHERE ml.menu_name = :menu
-    ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC";
-  $result = db_query($sql, array(':menu' => $menu['menu_name']), array('fetch' => PDO::FETCH_ASSOC));
   $links = array();
-  foreach ($result as $item) {
-    $links[] = $item;
+  $query = entity_query('menu_link')
+    ->condition('menu_name', $menu['menu_name']);
+  for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) {
+    $query->sort('p' . $i, 'ASC');
   }
+  $result = $query->execute();
+
+  if (!empty($result)) {
+    $links = menu_link_load_multiple($result);
+  }
+
   $delta = max(count($links), 50);
   $tree = menu_tree_data($links);
   $node_links = array();
@@ -291,193 +294,6 @@ function theme_menu_overview_form($variables) {
 }
 
 /**
- * Menu callback; Build the menu link editing form.
- */
-function menu_edit_item($form, &$form_state, $type, $item, $menu) {
-  if ($type == 'add' || empty($item)) {
-    // This is an add form, initialize the menu link.
-    $item = array('link_title' => '', 'mlid' => 0, 'plid' => 0, 'menu_name' => $menu['menu_name'], 'weight' => 0, 'link_path' => '', 'options' => array(), 'module' => 'menu', 'expanded' => 0, 'hidden' => 0, 'has_children' => 0);
-  }
-  else {
-    // Get the human-readable menu title from the given menu name.
-    $titles = menu_get_menus();
-    $current_title = $titles[$item['menu_name']];
-
-    // Get the current breadcrumb and add a link to that menu's overview page.
-    $breadcrumb = menu_get_active_breadcrumb();
-    $breadcrumb[] = l($current_title, 'admin/structure/menu/manage/' . $item['menu_name']);
-    drupal_set_breadcrumb($breadcrumb);
-  }
-  $form['actions'] = array('#type' => 'actions');
-  $form['link_title'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Menu link title'),
-    '#default_value' => $item['link_title'],
-    '#description' => t('The text to be used for this link in the menu.'),
-    '#required' => TRUE,
-  );
-  foreach (array('link_path', 'mlid', 'module', 'has_children', 'options') as $key) {
-    $form[$key] = array('#type' => 'value', '#value' => $item[$key]);
-  }
-  // Any item created or edited via this interface is considered "customized".
-  $form['customized'] = array('#type' => 'value', '#value' => 1);
-  $form['original_item'] = array('#type' => 'value', '#value' => $item);
-
-  $path = $item['link_path'];
-  if (isset($item['options']['query'])) {
-    $path .= '?' . drupal_http_build_query($item['options']['query']);
-  }
-  if (isset($item['options']['fragment'])) {
-    $path .= '#' . $item['options']['fragment'];
-  }
-  if ($item['module'] == 'menu') {
-    $form['link_path'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Path'),
-      '#maxlength' => 255,
-      '#default_value' => $path,
-      '#description' => t('The path for this menu link. This can be an internal Drupal path such as %add-node or an external URL such as %drupal. Enter %front to link to the front page.', array('%front' => '<front>', '%add-node' => 'node/add', '%drupal' => 'http://drupal.org')),
-      '#required' => TRUE,
-    );
-    $form['actions']['delete'] = array(
-      '#type' => 'submit',
-      '#value' => t('Delete'),
-      '#access' => $item['mlid'],
-      '#submit' => array('menu_item_delete_submit'),
-      '#weight' => 10,
-    );
-  }
-  else {
-    $form['_path'] = array(
-      '#type' => 'item',
-      '#title' => t('Path'),
-      '#description' => l($item['link_title'], $item['href'], $item['options']),
-    );
-  }
-  $form['description'] = array(
-    '#type' => 'textarea',
-    '#title' => t('Description'),
-    '#default_value' => isset($item['options']['attributes']['title']) ? $item['options']['attributes']['title'] : '',
-    '#rows' => 1,
-    '#description' => t('Shown when hovering over the menu link.'),
-  );
-  $form['enabled'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Enabled'),
-    '#default_value' => !$item['hidden'],
-    '#description' => t('Menu links that are not enabled will not be listed in any menu.'),
-  );
-  $form['expanded'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Show as expanded'),
-    '#default_value' => $item['expanded'],
-    '#description' => t('If selected and this menu link has children, the menu will always appear expanded.'),
-  );
-
-  // Generate a list of possible parents (not including this link or descendants).
-  $options = menu_parent_options(menu_get_menus(), $item);
-  $default = $item['menu_name'] . ':' . $item['plid'];
-  if (!isset($options[$default])) {
-    $default = 'tools:0';
-  }
-  $form['parent'] = array(
-    '#type' => 'select',
-    '#title' => t('Parent link'),
-    '#default_value' => $default,
-    '#options' => $options,
-    '#description' => t('The maximum depth for a link and all its children is fixed at !maxdepth. Some menu links may not be available as parents if selecting them would exceed this limit.', array('!maxdepth' => MENU_MAX_DEPTH)),
-    '#attributes' => array('class' => array('menu-title-select')),
-  );
-
-  // Get number of items in menu so the weight selector is sized appropriately.
-  $sql = "SELECT COUNT(*) FROM {menu_links} WHERE menu_name = :menu";
-  $result = db_query($sql, array(':menu' => $item['menu_name']), array('fetch' => PDO::FETCH_ASSOC));
-  foreach ($result as $row) {
-    foreach ($row as $menu_item_count) {
-      $delta = $menu_item_count;
-    }
-  }
-  if ($delta < 50) {
-    // Old hardcoded value.
-    $delta = 50;
-  }
-  $form['weight'] = array(
-    '#type' => 'weight',
-    '#title' => t('Weight'),
-    '#delta' => $delta,
-    '#default_value' => $item['weight'],
-    '#description' => t('Optional. In the menu, the heavier links will sink and the lighter links will be positioned nearer the top.'),
-  );
-  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save'), '#button_type' => 'primary');
-
-  return $form;
-}
-
-/**
- * Validate form values for a menu link being added or edited.
- */
-function menu_edit_item_validate($form, &$form_state) {
-  $item = &$form_state['values'];
-  $normal_path = drupal_container()->get('path.alias_manager')->getSystemPath($item['link_path']);
-  if ($item['link_path'] != $normal_path) {
-    drupal_set_message(t('The menu system stores system paths only, but will use the URL alias for display. %link_path has been stored as %normal_path', array('%link_path' => $item['link_path'], '%normal_path' => $normal_path)));
-    $item['link_path'] = $normal_path;
-  }
-  if (!url_is_external($item['link_path'])) {
-    $parsed_link = parse_url($item['link_path']);
-    if (isset($parsed_link['query'])) {
-      $item['options']['query'] = drupal_get_query_array($parsed_link['query']);
-    }
-    else {
-      // Use unset() rather than setting to empty string
-      // to avoid redundant serialized data being stored.
-      unset($item['options']['query']);
-    }
-    if (isset($parsed_link['fragment'])) {
-      $item['options']['fragment'] = $parsed_link['fragment'];
-    }
-    else {
-      unset($item['options']['fragment']);
-    }
-    if (isset($parsed_link['path']) && $item['link_path'] != $parsed_link['path']) {
-      $item['link_path'] = $parsed_link['path'];
-    }
-  }
-  if (!trim($item['link_path']) || !drupal_valid_path($item['link_path'], TRUE)) {
-    form_set_error('link_path', t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $item['link_path'])));
-  }
-}
-
-/**
- * Submit function for the delete button on the menu item editing form.
- */
-function menu_item_delete_submit($form, &$form_state) {
-  $form_state['redirect'] = 'admin/structure/menu/item/' . $form_state['values']['mlid'] . '/delete';
-}
-
-/**
- * Process menu and menu item add/edit form submissions.
- */
-function menu_edit_item_submit($form, &$form_state) {
-  $item = &$form_state['values'];
-
-  // The value of "hidden" is the opposite of the value
-  // supplied by the "enabled" checkbox.
-  $item['hidden'] = (int) !$item['enabled'];
-  unset($item['enabled']);
-
-  $item['options']['attributes']['title'] = $item['description'];
-  list($item['menu_name'], $item['plid']) = explode(':', $item['parent']);
-  if (!menu_link_save($item)) {
-    drupal_set_message(t('There was an error saving the menu link.'), 'error');
-  }
-  else {
-    drupal_set_message(t('Your configuration has been saved.'));
-  }
-  $form_state['redirect'] = 'admin/structure/menu/manage/' . $item['menu_name'];
-}
-
-/**
  * Menu callback; Build the form that handles the adding/editing of a custom menu.
  */
 function menu_edit_menu($form, &$form_state, $type, $menu = array()) {
@@ -568,7 +384,7 @@ function menu_delete_menu_page($menu) {
 function menu_delete_menu_confirm($form, &$form_state, $menu) {
   $form['#menu'] = $menu;
   $caption = '';
-  $num_links = db_query("SELECT COUNT(*) FROM {menu_links} WHERE menu_name = :menu", array(':menu' => $menu['menu_name']))->fetchField();
+  $num_links = entity_get_controller('menu_link')->countMenuLinks($menu['menu_name']);
   if ($num_links) {
     $caption .= '<p>' . format_plural($num_links, '<strong>Warning:</strong> There is currently 1 menu link in %title. It will be deleted (system-defined items will be reset).', '<strong>Warning:</strong> There are currently @count menu links in %title. They will be deleted (system-defined links will be reset).', array('%title' => $menu['title'])) . '</p>';
   }
@@ -590,16 +406,16 @@ function menu_delete_menu_confirm_submit($form, &$form_state) {
   }
 
   // Reset all the menu links defined by the system via hook_menu().
-  $result = db_query("SELECT * FROM {menu_links} ml INNER JOIN {menu_router} m ON ml.router_path = m.path WHERE ml.menu_name = :menu AND ml.module = 'system' ORDER BY m.number_parts ASC", array(':menu' => $menu['menu_name']), array('fetch' => PDO::FETCH_ASSOC));
-  foreach ($result as $link) {
-    menu_reset_item($link);
+  // @todo Convert this to an EFQ once we figure out 'ORDER BY m.number_parts'.
+  $result = db_query("SELECT mlid FROM {menu_links} ml INNER JOIN {menu_router} m ON ml.router_path = m.path WHERE ml.menu_name = :menu AND ml.module = 'system' ORDER BY m.number_parts ASC", array(':menu' => $menu['menu_name']), array('fetch' => PDO::FETCH_ASSOC))->fetchCol();
+  $menu_links = menu_link_load_multiple($result);
+  foreach ($menu_links as $link) {
+    $link->reset();
   }
 
   // Delete all links to the overview page for this menu.
-  $result = db_query("SELECT mlid FROM {menu_links} ml WHERE ml.link_path = :link", array(':link' => 'admin/structure/menu/manage/' . $menu['menu_name']), array('fetch' => PDO::FETCH_ASSOC));
-  foreach ($result as $link) {
-    menu_link_delete($link['mlid']);
-  }
+  $menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => 'admin/structure/menu/manage/' . $menu['menu_name']));
+  menu_link_delete_multiple(array_keys($menu_links));
 
   // Delete the custom menu and all its menu links.
   menu_delete($menu);
@@ -619,7 +435,7 @@ function menu_edit_menu_name_exists($value) {
   // 'menu-' is added to the menu name to avoid name-space conflicts.
   $value = 'menu-' . $value;
   $custom_exists = db_query_range('SELECT 1 FROM {menu_custom} WHERE menu_name = :menu', 0, 1, array(':menu' => $value))->fetchField();
-  $link_exists = db_query_range("SELECT 1 FROM {menu_links} WHERE menu_name = :menu", 0, 1, array(':menu' => $value))->fetchField();
+  $link_exists = entity_query('menu_link')->condition('menu_name', $value)->range(0,1)->count()->execute();
 
   return $custom_exists || $link_exists;
 }
@@ -633,26 +449,23 @@ function menu_edit_menu_submit($form, &$form_state) {
   if ($form['#insert']) {
     // Add 'menu-' to the menu name to help avoid name-space conflicts.
     $menu['menu_name'] = 'menu-' . $menu['menu_name'];
-    $link['link_title'] = $menu['title'];
-    $link['link_path'] = $path . $menu['menu_name'];
-    $link['router_path'] = $path . '%';
-    $link['module'] = 'menu';
-    $link['plid'] = db_query("SELECT mlid FROM {menu_links} WHERE link_path = :link AND module = :module", array(
-      ':link' => 'admin/structure/menu',
-      ':module' => 'system'
-    ))
-    ->fetchField();
-
-    menu_link_save($link);
+    $system_link = entity_load_multiple_by_properties('menu_link', array('link_path' => 'admin/structure/menu', 'module' => 'system'));
+    $system_link = reset($system_link);
+    $menu_link = entity_create('menu_link', array(
+      'link_title' => $menu['title'],
+      'link_path' => $path . $menu['menu_name'],
+      'router_path' => $path . '%',
+      'plid' => $system_link->id(),
+    ));
+    $menu_link->save();
     menu_save($menu);
   }
   else {
     menu_save($menu);
-    $result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = :path", array(':path' => $path . $menu['menu_name']), array('fetch' => PDO::FETCH_ASSOC));
-    foreach ($result as $m) {
-      $link = menu_link_load($m['mlid']);
-      $link['link_title'] = $menu['title'];
-      menu_link_save($link);
+    $menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $path . $menu['menu_name']));
+    foreach ($menu_links as $menu_link) {
+      $menu_link->link_title = $menu['title'];
+      $menu_link->save();
     }
   }
   drupal_set_message(t('Your configuration has been saved.'));
@@ -660,53 +473,80 @@ function menu_edit_menu_submit($form, &$form_state) {
 }
 
 /**
- * Menu callback; Check access and present a confirm form for deleting a menu link.
+ * Menu callback: Provides the menu link submission form.
+ *
+ * @param array $menu
+ *   An array representing a custom menu.
+ *
+ * @return
+ *   Returns the menu link submission form.
  */
-function menu_item_delete_page($item) {
+function menu_link_add(array $menu) {
+  $menu_link = entity_create('menu_link', array(
+    'mlid' => 0,
+    'plid' => 0,
+    'menu_name' => $menu['menu_name'],
+  ));
+  drupal_set_title(t('Add menu link'));
+  return entity_get_form($menu_link);
+}
+
+/**
+ * Menu callback; Check access and present a confirm form for deleting a menu
+ * link.
+ */
+function menu_link_delete_page(MenuLink $menu_link) {
   // Links defined via hook_menu may not be deleted. Updated items are an
   // exception, as they can be broken.
-  if ($item['module'] == 'system' && !$item['updated']) {
+  if ($menu_link->module == 'system' && !$menu_link->updated) {
     throw new AccessDeniedHttpException();
   }
-  return drupal_get_form('menu_item_delete_form', $item);
+  return drupal_get_form('menu_link_delete_form', $menu_link);
 }
 
 /**
  * Build a confirm form for deletion of a single menu link.
  */
-function menu_item_delete_form($form, &$form_state, $item) {
-  $form['#item'] = $item;
-  return confirm_form($form, t('Are you sure you want to delete the custom menu link %item?', array('%item' => $item['link_title'])), 'admin/structure/menu/manage/' . $item['menu_name']);
+function menu_link_delete_form($form, &$form_state, MenuLink $menu_link) {
+  $form['#menu_link'] = $menu_link;
+  return confirm_form($form,
+    t('Are you sure you want to delete the custom menu link %item?', array('%item' => $menu_link->link_title)),
+    'admin/structure/menu/manage/' . $menu_link->menu_name
+  );
 }
 
 /**
- * Process menu delete form submissions.
+ * Processes menu link delete form submissions.
  */
-function menu_item_delete_form_submit($form, &$form_state) {
-  $item = $form['#item'];
-  menu_link_delete($item['mlid']);
-  $t_args = array('%title' => $item['link_title']);
+function menu_link_delete_form_submit($form, &$form_state) {
+  $menu_link = $form['#menu_link'];
+  menu_link_delete($menu_link->id());
+  $t_args = array('%title' => $menu_link->link_title);
   drupal_set_message(t('The menu link %title has been deleted.', $t_args));
   watchdog('menu', 'Deleted menu link %title.', $t_args, WATCHDOG_NOTICE);
-  $form_state['redirect'] = 'admin/structure/menu/manage/' . $item['menu_name'];
+  $form_state['redirect'] = 'admin/structure/menu/manage/' . $menu_link->menu_name;
 }
 
 /**
- * Menu callback; reset a single modified menu link.
+ * Menu callback; Reset a single modified menu link.
  */
-function menu_reset_item_confirm($form, &$form_state, $item) {
-  $form['item'] = array('#type' => 'value', '#value' => $item);
-  return confirm_form($form, t('Are you sure you want to reset the link %item to its default values?', array('%item' => $item['link_title'])), 'admin/structure/menu/manage/' . $item['menu_name'], t('Any customizations will be lost. This action cannot be undone.'), t('Reset'));
+function menu_link_reset_form($form, &$form_state, MenuLink $menu_link) {
+  $form['#menu_link'] = $menu_link;
+  return confirm_form($form,
+    t('Are you sure you want to reset the link %item to its default values?', array('%item' => $menu_link->link_title)),
+    'admin/structure/menu/manage/' . $menu_link->menu_name,
+    t('Any customizations will be lost. This action cannot be undone.'),
+    t('Reset'));
 }
 
 /**
- * Process menu reset item form submissions.
+ * Processes menu link reset form submissions.
  */
-function menu_reset_item_confirm_submit($form, &$form_state) {
-  $item = $form_state['values']['item'];
-  $new_item = menu_reset_item($item);
+function menu_link_reset_form_submit($form, &$form_state) {
+  $menu_link = $form['#menu_link'];
+  $new_menu_link = $menu_link->reset();
   drupal_set_message(t('The menu link was reset to its default settings.'));
-  $form_state['redirect'] = 'admin/structure/menu/manage/' . $new_item['menu_name'];
+  $form_state['redirect'] = 'admin/structure/menu/manage/' . $new_menu_link->menu_name;
 }
 
 /**
diff --git a/core/modules/menu/menu.info b/core/modules/menu/menu.info
index e5e2c8b..7aad518 100644
--- a/core/modules/menu/menu.info
+++ b/core/modules/menu/menu.info
@@ -3,4 +3,5 @@ description = Allows administrators to customize the site navigation menu.
 package = Core
 version = VERSION
 core = 8.x
+dependencies[] = menu_link
 configure = admin/structure/menu
diff --git a/core/modules/menu/menu.module b/core/modules/menu/menu.module
index e9080fd..5edff22 100644
--- a/core/modules/menu/menu.module
+++ b/core/modules/menu/menu.module
@@ -14,6 +14,8 @@
 use Drupal\node\Plugin\Core\Entity\Node;
 
 use Symfony\Component\HttpFoundation\JsonResponse;
+use Drupal\menu_link\Plugin\Core\Entity\MenuLink;
+use Drupal\menu_link\MenuLinkStorageController;
 
 /**
  * Maximum length of menu name as entered by the user. Database length is 32
@@ -113,9 +115,9 @@ function menu_menu() {
     'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
   );
   $items['admin/structure/menu/manage/%menu/add'] = array(
-    'title' => 'Add link',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('menu_edit_item', 'add', NULL, 4),
+    'title' => 'Add menu link',
+    'page callback' => 'menu_link_add',
+    'page arguments' => array(4),
     'access arguments' => array('administer menu'),
     'type' => MENU_LOCAL_ACTION,
     'file' => 'menu.admin.inc',
@@ -138,21 +140,20 @@ function menu_menu() {
   );
   $items['admin/structure/menu/item/%menu_link/edit'] = array(
     'title' => 'Edit menu link',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('menu_edit_item', 'edit', 4, NULL),
+    'page callback' => 'entity_get_form',
+    'page arguments' => array(4),
     'access arguments' => array('administer menu'),
-    'file' => 'menu.admin.inc',
   );
   $items['admin/structure/menu/item/%menu_link/reset'] = array(
     'title' => 'Reset menu link',
     'page callback' => 'drupal_get_form',
-    'page arguments' => array('menu_reset_item_confirm', 4),
+    'page arguments' => array('menu_link_reset_form', 4),
     'access arguments' => array('administer menu'),
     'file' => 'menu.admin.inc',
   );
   $items['admin/structure/menu/item/%menu_link/delete'] = array(
     'title' => 'Delete menu link',
-    'page callback' => 'menu_item_delete_page',
+    'page callback' => 'menu_link_delete_page',
     'page arguments' => array(4),
     'access arguments' => array('administer menu'),
     'file' => 'menu.admin.inc',
@@ -183,23 +184,29 @@ function menu_theme() {
  */
 function menu_enable() {
   menu_router_rebuild();
-  $base_link = db_query("SELECT mlid AS plid, menu_name FROM {menu_links} WHERE link_path = 'admin/structure/menu' AND module = 'system'")->fetchAssoc();
-  $base_link['router_path'] = 'admin/structure/menu/manage/%';
-  $base_link['module'] = 'menu';
+  $system_link = entity_load_multiple_by_properties('menu_link', array('link_path' => 'admin/structure/menu', 'module' => 'system'));
+  $system_link = reset($system_link);
+
+  $base_link = entity_create('menu_link', array(
+    'menu_name' => $system_link->menu_name,
+    'router_path' => 'admin/structure/menu/manage/%',
+    'module' => 'menu',
+  ));
+
   $result = db_query("SELECT * FROM {menu_custom}", array(), array('fetch' => PDO::FETCH_ASSOC));
   foreach ($result as $menu) {
-    // $link is passed by reference to menu_link_save(), so we make a copy of $base_link.
-    $link = $base_link;
-    $link['mlid'] = 0;
-    $link['link_title'] = $menu['title'];
-    $link['link_path'] = 'admin/structure/menu/manage/' . $menu['menu_name'];
-    $menu_link = db_query("SELECT mlid FROM {menu_links} WHERE link_path = :path AND plid = :plid", array(
-      ':path' => $link['link_path'],
-      ':plid' => $link['plid']
-    ))
-    ->fetchField();
-    if (!$menu_link) {
-      menu_link_save($link);
+    $link = $base_link->createDuplicate();
+    $link->plid = $system_link->id();
+    $link->link_title = $menu['title'];
+    $link->link_path = 'admin/structure/menu/manage/' . $menu['menu_name'];
+
+    $query = entity_query('menu_link')
+      ->condition('link_path', $link->link_path)
+      ->condition('plid', $link->plid);
+    $result = $query->execute();
+
+    if (empty($result)) {
+      $link->save();
     }
   }
   menu_cache_clear_all();
@@ -339,24 +346,26 @@ function menu_delete($menu) {
 }
 
 /**
- * Return a list of menu items that are valid possible parents for the given menu item.
+ * Returns a list of menu links that are valid possible parents for the given
+ * menu link.
  *
- * @param $menus
+ * @param array $menus
  *   An array of menu names and titles, such as from menu_get_menus().
- * @param $item
- *   The menu item or the node type for which to generate a list of parents.
- *   If $item['mlid'] == 0 then the complete tree is returned.
- * @param $type
+ * @param \Drupal\menu_link\Plugin\Core\Entity\MenuLink $menu_link
+ *   The menu link for which to generate a list of parents.
+ *   If $menu_link->id() == 0 then the complete tree is returned.
+ * @param string $type
  *   The node type for which to generate a list of parents.
  *   If $item itself is a node type then $type is ignored.
- * @return
- *   An array of menu link titles keyed on the a string containing the menu name
- *   and mlid. The list excludes the given item and its children.
+ *
+ * @return array
+ *   An array of menu link titles keyed by a string containing the menu name and
+ *   mlid. The list excludes the given item and its children.
  *
  * @todo This has to be turned into a #process form element callback. The
  *   'menu_override_parent_selector' variable is entirely superfluous.
  */
-function menu_parent_options($menus, $item, $type = '') {
+function menu_parent_options(array $menus, MenuLink $menu_link = NULL, $type = NULL) {
   // The menu_links table can be practically any size and we need a way to
   // allow contrib modules to provide more scalable pattern choosers.
   // hook_form_alter is too late in itself because all the possible parents are
@@ -365,14 +374,12 @@ function menu_parent_options($menus, $item, $type = '') {
     return array();
   }
 
-  $available_menus = array();
-  if (!is_array($item)) {
-    // If $item is not an array then it is a node type.
-    // Use it as $type and prepare a dummy menu item for _menu_get_options().
-    $type = $item;
-    $item = array('mlid' => 0);
+  if (!$menu_link) {
+    $menu_link = entity_create('menu_link', array('mlid' => 0));
   }
-  if (empty($type)) {
+
+  $available_menus = array();
+  if (!$type) {
     // If no node type is set, use all menus given to this function.
     $available_menus = $menus;
   }
@@ -384,7 +391,7 @@ function menu_parent_options($menus, $item, $type = '') {
     }
   }
 
-  return _menu_get_options($menus, $available_menus, $item);
+  return _menu_get_options($menus, $available_menus, $menu_link);
 }
 
 /**
@@ -449,26 +456,6 @@ function _menu_parents_recurse($tree, $menu_name, $indent, &$options, $exclude,
 }
 
 /**
- * Reset a system-defined menu link.
- */
-function menu_reset_item($link) {
-  // To reset the link to its original values, we need to retrieve its
-  // definition from hook_menu(). Otherwise, for example, the link's menu would
-  // not be reset, because properties like the original 'menu_name' are not
-  // stored anywhere else. Since resetting a link happens rarely and this is a
-  // one-time operation, retrieving the full menu router does no harm.
-  $menu = menu_get_router();
-  $router_item = $menu[$link['router_path']];
-  $new_link = _menu_link_build($router_item);
-  // Merge existing menu link's ID and 'has_children' property.
-  foreach (array('mlid', 'has_children') as $key) {
-    $new_link[$key] = $link[$key];
-  }
-  menu_link_save($new_link);
-  return $new_link;
-}
-
-/**
  * Implements hook_block_info().
  */
 function menu_block_info() {
@@ -532,7 +519,7 @@ function menu_node_save(Node $node) {
   if (isset($node->menu)) {
     $link = &$node->menu;
     if (empty($link['enabled'])) {
-      if (!empty($link['mlid'])) {
+      if (!$link->isNew()) {
         menu_link_delete($link['mlid']);
       }
     }
@@ -559,9 +546,13 @@ function menu_node_save(Node $node) {
  */
 function menu_node_predelete(Node $node) {
   // Delete all menu module links that point to this node.
-  $result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = :path AND module = 'menu'", array(':path' => 'node/' . $node->nid), array('fetch' => PDO::FETCH_ASSOC));
-  foreach ($result as $m) {
-    menu_link_delete($m['mlid']);
+  $query = entity_query('menu_link')
+    ->condition('link_path', 'node/' . $node->nid)
+    ->condition('module', 'menu');
+  $result = $query->execute();
+
+  if (!empty($result)) {
+    menu_link_delete_multiple($result);
   }
 }
 
@@ -572,42 +563,48 @@ function menu_node_prepare(Node $node) {
   if (empty($node->menu)) {
     // Prepare the node for the edit form so that $node->menu always exists.
     $menu_name = strtok(variable_get('menu_parent_' . $node->type, 'main:0'), ':');
-    $item = array();
+    $menu_link = FALSE;
     if (isset($node->nid)) {
       $mlid = FALSE;
       // Give priority to the default menu
       $type_menus = variable_get('menu_options_' . $node->type, array('main' => 'main'));
       if (in_array($menu_name, $type_menus)) {
-        $mlid = db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = :path AND menu_name = :menu_name AND module = 'menu' ORDER BY mlid ASC", 0, 1, array(
-          ':path' => 'node/' . $node->nid,
-          ':menu_name' => $menu_name,
-        ))->fetchField();
+        $query = entity_query('menu_link')
+          ->condition('link_path', 'node/' . $node->nid)
+          ->condition('menu_name', $menu_name)
+          ->condition('module', 'menu')
+          ->sort('mlid', 'ASC')
+          ->range(0, 1);
+        $result = $query->execute();
+
+        $mlid = (!empty($result)) ? reset($result) : FALSE;
       }
       // Check all allowed menus if a link does not exist in the default menu.
       if (!$mlid && !empty($type_menus)) {
-        $mlid = db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = :path AND module = 'menu' AND menu_name IN (:type_menus) ORDER BY mlid ASC", 0, 1, array(
-          ':path' => 'node/' . $node->nid,
-          ':type_menus' => array_values($type_menus),
-        ))->fetchField();
+        $query = entity_query('menu_link')
+          ->condition('link_path', 'node/' . $node->nid)
+          ->condition('menu_name', array_values($type_menus), 'IN')
+          ->condition('module', 'menu')
+          ->sort('mlid', 'ASC')
+          ->range(0, 1);
+        $result = $query->execute();
+
+        $mlid = (!empty($result)) ? reset($result) : FALSE;
       }
       if ($mlid) {
-        $item = menu_link_load($mlid);
+        $menu_link = menu_link_load($mlid);
       }
     }
+
+    if (!$menu_link) {
+      $menu_link = entity_create('menu_link', array(
+        'mlid' => 0,
+        'plid' => 0,
+        'menu_name' => $menu_name,
+      ));
+    }
     // Set default values.
-    $node->menu = $item + array(
-      'link_title' => '',
-      'mlid' => 0,
-      'plid' => 0,
-      'menu_name' => $menu_name,
-      'weight' => 0,
-      'options' => array(),
-      'module' => 'menu',
-      'expanded' => 0,
-      'hidden' => 0,
-      'has_children' => 0,
-      'customized' => 0,
-    );
+    $node->menu = $menu_link;
   }
   // Find the depth limit for the parent select.
   if (!isset($node->menu['parent_depth_limit'])) {
@@ -619,7 +616,7 @@ function menu_node_prepare(Node $node) {
  * Find the depth limit for items in the parent select.
  */
 function _menu_parent_depth_limit($item) {
-  return MENU_MAX_DEPTH - 1 - (($item['mlid'] && $item['has_children']) ? menu_link_children_relative_depth($item) : 0);
+  return MENU_MAX_DEPTH - 1 - (($item['mlid'] && $item['has_children']) ? entity_get_controller('menu_link')->findChildrenRelativeDepth($item) : 0);
 }
 
 /**
@@ -635,10 +632,7 @@ function menu_form_node_form_alter(&$form, $form_state) {
   $node = $form_state['controller']->getEntity($form_state);
   $link = $node->menu;
   $type = $node->type;
-  // menu_parent_options() is goofy and can actually handle either a menu link
-  // or a node type both as second argument. Pick based on whether there is
-  // a link already (menu_node_prepare() sets mlid default to 0).
-  $options = menu_parent_options(menu_get_menus(), $link['mlid'] ? $link : $type, $type);
+  $options = menu_parent_options(menu_get_menus(), $link, $type);
   // If no possible parent menu items were found, there is nothing to display.
   if (empty($options)) {
     return;
@@ -710,13 +704,7 @@ function menu_form_node_form_alter(&$form, $form_state) {
   );
 
   // Get number of items in menu so the weight selector is sized appropriately.
-  $sql = "SELECT COUNT(*) FROM {menu_links} WHERE menu_name = :menu";
-  $result = db_query($sql, array(':menu' => $link['menu_name']), array('fetch' => PDO::FETCH_ASSOC));
-  foreach ($result as $row) {
-    foreach ($row as $menu_items) {
-      $delta = $menu_items;
-    }
-  }
+  $delta = entity_get_controller('menu_link')->countMenuLinks($link->menu_name);
   if ($delta < 50) {
     // Old hardcoded value
     $delta = 50;
@@ -736,6 +724,7 @@ function menu_form_node_form_alter(&$form, $form_state) {
  * @see menu_form_node_form_alter()
  */
 function menu_node_submit(Node $node, $form, $form_state) {
+  $node->menu = entity_create('menu_link', $form_state['values']['menu']);
   // Decompose the selected menu parent option into 'menu_name' and 'plid', if
   // the form used the default parent selection widget.
   if (!empty($form_state['values']['menu']['parent'])) {
@@ -772,7 +761,8 @@ function menu_form_node_type_form_alter(&$form, $form_state) {
   // all available menu items.
   // Otherwise it is not possible to dynamically add options to the list.
   // @todo Convert menu_parent_options() into a #process callback.
-  $options = menu_parent_options(menu_get_menus(), array('mlid' => 0));
+  $menu_link = entity_create('menu_link', array('mlid' => 0));
+  $options = menu_parent_options(menu_get_menus(), $menu_link);
   $form['menu']['menu_parent'] = array(
     '#type' => 'select',
     '#title' => t('Default parent item'),
diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkFormController.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkFormController.php
new file mode 100644
index 0000000..fb8aa06
--- /dev/null
+++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkFormController.php
@@ -0,0 +1,228 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_link\MenuLinkFormController.
+ */
+
+namespace Drupal\menu_link;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityFormController;
+
+/**
+ * Form controller for the node edit forms.
+ */
+class MenuLinkFormController extends EntityFormController {
+
+  /**
+   * Overrides Drupal\Core\Entity\EntityFormController::form().
+   */
+  public function form(array $form, array &$form_state, EntityInterface $menu_link) {
+    // Since menu_link_load() no longer returns a translated and access checked
+    // item, do it here instead.
+    _menu_link_translate($menu_link);
+
+    if (!$menu_link->isNew()) {
+      // Get the human-readable menu title from the given menu name.
+      $titles = menu_get_menus();
+      $current_title = $titles[$menu_link->menu_name];
+
+      // Get the current breadcrumb and add a link to that menu's overview page.
+      $breadcrumb = menu_get_active_breadcrumb();
+      $breadcrumb[] = l($current_title, 'admin/structure/menu/manage/' . $menu_link->menu_name);
+      drupal_set_breadcrumb($breadcrumb);
+    }
+
+    $form['link_title'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Menu link title'),
+      '#default_value' => $menu_link->link_title,
+      '#description' => t('The text to be used for this link in the menu.'),
+      '#required' => TRUE,
+    );
+    foreach (array('link_path', 'mlid', 'module', 'has_children', 'options') as $key) {
+      $form[$key] = array('#type' => 'value', '#value' => $menu_link->{$key});
+    }
+    // Any item created or edited via this interface is considered "customized".
+    $form['customized'] = array('#type' => 'value', '#value' => 1);
+
+    // We are not using url() when constructing this path because it would add
+    // $base_path.
+    $path = $menu_link->link_path;
+    if (isset($menu_link->options['query'])) {
+      $path .= '?' . drupal_http_build_query($menu_link->options['query']);
+    }
+    if (isset($menu_link->options['fragment'])) {
+      $path .= '#' . $menu_link->options['fragment'];
+    }
+    if ($menu_link->module == 'menu') {
+      $form['link_path'] = array(
+        '#type' => 'textfield',
+        '#title' => t('Path'),
+        '#maxlength' => 255,
+        '#default_value' => $path,
+        '#description' => t('The path for this menu link. This can be an internal Drupal path such as %add-node or an external URL such as %drupal. Enter %front to link to the front page.', array('%front' => '<front>', '%add-node' => 'node/add', '%drupal' => 'http://drupal.org')),
+        '#required' => TRUE,
+      );
+    }
+    else {
+      $form['_path'] = array(
+        '#type' => 'item',
+        '#title' => t('Path'),
+        '#description' => l($menu_link->link_title, $menu_link->href, $menu_link->options),
+      );
+    }
+
+    $form['description'] = array(
+      '#type' => 'textarea',
+      '#title' => t('Description'),
+      '#default_value' => isset($menu_link->options['attributes']['title']) ? $menu_link->options['attributes']['title'] : '',
+      '#rows' => 1,
+      '#description' => t('Shown when hovering over the menu link.'),
+    );
+    $form['enabled'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Enabled'),
+      '#default_value' => !$menu_link->hidden,
+      '#description' => t('Menu links that are not enabled will not be listed in any menu.'),
+    );
+    $form['expanded'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Show as expanded'),
+      '#default_value' => $menu_link->expanded,
+      '#description' => t('If selected and this menu link has children, the menu will always appear expanded.'),
+    );
+
+    // Generate a list of possible parents (not including this link or descendants).
+    $options = menu_parent_options(menu_get_menus(), $menu_link);
+    $default = $menu_link->menu_name . ':' . $menu_link->plid;
+    if (!isset($options[$default])) {
+      $default = 'tools:0';
+    }
+    $form['parent'] = array(
+      '#type' => 'select',
+      '#title' => t('Parent link'),
+      '#default_value' => $default,
+      '#options' => $options,
+      '#description' => t('The maximum depth for a link and all its children is fixed at !maxdepth. Some menu links may not be available as parents if selecting them would exceed this limit.', array('!maxdepth' => MENU_MAX_DEPTH)),
+      '#attributes' => array('class' => array('menu-title-select')),
+    );
+
+    // Get number of items in menu so the weight selector is sized appropriately.
+    $delta = entity_get_controller('menu_link')->countMenuLinks($menu_link->menu_name);
+    if ($delta < 50) {
+      // Old hardcoded value.
+      $delta = 50;
+    }
+    $form['weight'] = array(
+      '#type' => 'weight',
+      '#title' => t('Weight'),
+      '#delta' => $delta,
+      '#default_value' => $menu_link->weight,
+      '#description' => t('Optional. In the menu, the heavier links will sink and the lighter links will be positioned nearer the top.'),
+    );
+
+    $form['langcode'] = array(
+      '#type' => 'language_select',
+      '#title' => t('Language'),
+      '#languages' => LANGUAGE_ALL,
+      '#default_value' => $menu_link->langcode,
+    );
+
+    return parent::form($form, $form_state, $menu_link);
+  }
+
+  /**
+   * Overrides Drupal\Core\Entity\EntityFormController::actions().
+   */
+  protected function actions(array $form, array &$form_state) {
+    $element = parent::actions($form, $form_state);
+    $element['submit']['#button_type'] = 'primary';
+    $element['delete']['#access'] = $this->getEntity($form_state)->module == 'menu';
+
+    return $element;
+  }
+
+  /**
+   * Overrides Drupal\Core\Entity\EntityFormController::validate().
+   */
+  public function validate(array $form, array &$form_state) {
+    $menu_link = $this->buildEntity($form, $form_state);
+
+    $normal_path = drupal_container()->get('path.alias_manager')->getSystemPath($menu_link->link_path);
+    if ($menu_link->link_path != $normal_path) {
+      drupal_set_message(t('The menu system stores system paths only, but will use the URL alias for display. %link_path has been stored as %normal_path', array('%link_path' => $menu_link->link_path, '%normal_path' => $normal_path)));
+      $menu_link->link_path = $normal_path;
+    }
+    if (!url_is_external($menu_link->link_path)) {
+      $parsed_link = parse_url($menu_link->link_path);
+      if (isset($parsed_link['query'])) {
+        $menu_link->options['query'] = drupal_get_query_array($parsed_link['query']);
+      }
+      else {
+        // Use unset() rather than setting to empty string
+        // to avoid redundant serialized data being stored.
+        unset($menu_link->options['query']);
+      }
+      if (isset($parsed_link['fragment'])) {
+        $menu_link->options['fragment'] = $parsed_link['fragment'];
+      }
+      else {
+        unset($menu_link->options['fragment']);
+      }
+      if (isset($parsed_link['path']) && $menu_link->link_path != $parsed_link['path']) {
+        $menu_link->link_path = $parsed_link['path'];
+      }
+    }
+    if (!trim($menu_link->link_path) || !drupal_valid_path($menu_link->link_path, TRUE)) {
+      form_set_error('link_path', t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $menu_link->link_path)));
+    }
+
+    parent::validate($form, $form_state);
+  }
+
+  /**
+   * Overrides Drupal\Core\Entity\EntityFormController::submit().
+   */
+  public function submit(array $form, array &$form_state) {
+    // Build the menu link object from the submitted values.
+    $menu_link = parent::submit($form, $form_state);
+
+    // The value of "hidden" is the opposite of the value supplied by the
+    // "enabled" checkbox.
+    $menu_link->hidden = (int) !$menu_link->enabled;
+    unset($menu_link->enabled);
+
+    $menu_link->options['attributes']['title'] = $menu_link->description;
+    list($menu_link->menu_name, $menu_link->plid) = explode(':', $menu_link->parent);
+
+    return $menu_link;
+  }
+
+  /**
+   * Overrides Drupal\Core\Entity\EntityFormController::save().
+   */
+  public function save(array $form, array &$form_state) {
+    $menu_link = $this->getEntity($form_state);
+
+    $saved = $menu_link->save();
+
+    if ($saved) {
+      drupal_set_message(t('The menu link has been saved.'));
+      $form_state['redirect'] = 'admin/structure/menu/manage/' . $menu_link->menu_name;
+    }
+    else {
+      drupal_set_message(t('There was an error saving the menu link.'), 'error');
+      $form_state['rebuild'] = TRUE;
+    }
+  }
+
+  /**
+   * Overrides Drupal\Core\Entity\EntityFormController::delete().
+   */
+  public function delete(array $form, array &$form_state) {
+    $menu_link = $this->getEntity($form_state);
+    $form_state['redirect'] = 'admin/structure/menu/item/' . $menu_link->id() . '/delete';
+  }
+}
diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php
new file mode 100644
index 0000000..8ae2ecc
--- /dev/null
+++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php
@@ -0,0 +1,562 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_link\MenuLinkStorageController.
+ */
+
+namespace Drupal\menu_link;
+
+use Drupal\Core\Entity\DatabaseStorageController;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageException;
+
+/**
+ * Controller class for menu links.
+ *
+ * This extends the Drupal\entity\DatabaseStorageController class, adding
+ * required special handling for menu_link entities.
+ */
+class MenuLinkStorageController extends DatabaseStorageController {
+
+  /**
+   * @var bool
+   */
+  protected $preventReparenting = FALSE;
+
+  /**
+   * Holds an array of router item schema fields.
+   *
+   * @var array
+   */
+  protected static $routerItemFields = array();
+
+  /**
+   * Overrides Drupal\Core\Entity\DatabaseStorageController::__construct().
+   */
+  public function __construct($entityType) {
+    parent::__construct($entityType);
+
+    if (empty(self::$routerItemFields)) {
+      self::$routerItemFields = array_diff(drupal_schema_fields_sql('menu_router'), array('weight'));
+    }
+  }
+
+  /**
+   * Overrides Drupal\entity\DatabaseStorageController::buildQuery().
+   */
+  protected function buildQuery($ids, $revision_id = FALSE) {
+    $query = parent::buildQuery($ids, $revision_id);
+    // Specify additional fields from the {menu_router} table.
+    $query->leftJoin('menu_router', 'm', 'base.router_path = m.path');
+    $query->fields('m', self::$routerItemFields);
+    return $query;
+  }
+
+  /**
+   * Overrides Drupal\entity\DatabaseStorageController::attachLoad().
+   *
+   * @todo Don't call parent::attachLoad() at all because we want to be able to
+   * control the entity load hooks.
+   */
+  protected function attachLoad(&$menu_links, $load_revision = FALSE) {
+    foreach ($menu_links as &$menu_link) {
+      $menu_link->options = unserialize($menu_link->options);
+
+      // Use the weight property from the menu link.
+      $menu_link->router_item['weight'] = $menu_link->weight;
+    }
+
+    parent::attachLoad($menu_links, $load_revision);
+  }
+
+  /**
+   * Overrides Drupal\entity\DatabaseStorageController::save().
+   */
+  public function save(EntityInterface $entity) {
+    // We return SAVED_UPDATED by default because the logic below might not
+    // update the entity if its values haven't changed, so returning FALSE
+    // would be confusing in that situation.
+    $return = SAVED_UPDATED;
+
+    $transaction = db_transaction();
+    try {
+      // Load the stored entity, if any.
+      if (!$entity->isNew() && !isset($entity->original)) {
+        $entity->original = entity_load_unchanged($this->entityType, $entity->id());
+      }
+
+      if ($entity->isNew()) {
+        $entity->mlid = db_insert($this->entityInfo['base_table'])->fields(array('menu_name' => 'tools'))->execute();
+        $entity->enforceIsNew();
+      }
+
+      // Unlike the save() method from DatabaseStorageController, we invoke the
+      // 'presave' hook first because we want to allow modules to alter the
+      // entity before all the logic from our preSave() method.
+      $this->invokeHook('presave', $entity);
+      $this->preSave($entity);
+
+      // If every value in $entity->original is the same in the $entity, there
+      // is no reason to run the update queries or clear the caches. We use
+      // array_intersect_key() with the $entity as the first parameter because
+      // $entity may have additional keys left over from building a router entry.
+      // The intersect removes the extra keys, allowing a meaningful comparison.
+      if ($entity->isNew() || (array_intersect_key(get_object_vars($entity), get_object_vars($entity->original)) != get_object_vars($entity->original))) {
+        $return = drupal_write_record($this->entityInfo['base_table'], $entity, $this->idKey);
+
+        if ($return) {
+          if (!$entity->isNew()) {
+            $this->resetCache(array($entity->{$this->idKey}));
+            $this->postSave($entity, TRUE);
+            $this->invokeHook('update', $entity);
+          }
+          else {
+            $return = SAVED_NEW;
+            $this->resetCache();
+
+            $entity->enforceIsNew(FALSE);
+            $this->postSave($entity, FALSE);
+            $this->invokeHook('insert', $entity);
+          }
+        }
+      }
+
+      // Ignore slave server temporarily.
+      db_ignore_slave();
+      unset($entity->original);
+
+      return $return;
+    }
+    catch (\Exception $e) {
+      $transaction->rollback();
+      watchdog_exception($this->entityType, $e);
+      throw new EntityStorageException($e->getMessage(), $e->getCode(), $e);
+    }
+  }
+
+  /**
+   * Overrides Drupal\entity\DatabaseStorageController::preSave().
+   */
+  protected function preSave(EntityInterface $entity) {
+    // This is the easiest way to handle the unique internal path '<front>',
+    // since a path marked as external does not need to match a router path.
+    $entity->external = (url_is_external($entity->link_path) || $entity->link_path == '<front>') ? 1 : 0;
+
+    // Try to find a parent link. If found, assign it and derive its menu.
+    $parent_candidates = !empty($entity->parentCandidates) ? $entity->parentCandidates : array();
+    $parent = $this->findParent($entity, $parent_candidates);
+    if ($parent) {
+      $entity->plid = $parent->id();
+      $entity->menu_name = $parent->menu_name;
+    }
+    // If no corresponding parent link was found, move the link to the top-level.
+    else {
+      $entity->plid = 0;
+    }
+
+    // Directly fill parents for top-level links.
+    if ($entity->plid == 0) {
+      $entity->p1 = $entity->id();
+      for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) {
+        $parent_property = "p$i";
+        $entity->$parent_property = 0;
+      }
+      $entity->depth = 1;
+    }
+    // Otherwise, ensure that this link's depth is not beyond the maximum depth
+    // and fill parents based on the parent link.
+    else {
+      if ($entity->has_children && $entity->original) {
+        $limit = MENU_MAX_DEPTH - $this->findChildrenRelativeDepth($entity->original) - 1;
+      }
+      else {
+        $limit = MENU_MAX_DEPTH - 1;
+      }
+      if ($parent->depth > $limit) {
+        return FALSE;
+      }
+      $entity->depth = $parent->depth + 1;
+      $this->setParents($entity, $parent);
+    }
+
+    // Need to check both plid and menu_name, since plid can be 0 in any menu.
+    if (isset($entity->original) && ($entity->plid != $entity->original->plid || $entity->menu_name != $entity->original->menu_name)) {
+      $this->moveChildren($entity, $entity->original);
+    }
+    // Find the router_path.
+    if (empty($entity->router_path) || empty($entity->original) || (isset($entity->original) && $entity->original->link_path != $entity->link_path)) {
+      if ($entity->external) {
+        $entity->router_path = '';
+      }
+      else {
+        // Find the router path which will serve this path.
+        $entity->parts = explode('/', $entity->link_path, MENU_MAX_PARTS);
+        $entity->router_path = _menu_find_router_path($entity->link_path);
+      }
+    }
+  }
+
+  /**
+   * Overrides Drupal\entity\DatabaseStorageController::postSave().
+   */
+  function postSave(EntityInterface $entity, $update) {
+    // Check the has_children status of the parent.
+    $this->updateParentalStatus($entity);
+
+    menu_cache_clear($entity->menu_name);
+    if (isset($entity->original) && $entity->menu_name != $entity->original->menu_name) {
+      menu_cache_clear($entity->original->menu_name);
+    }
+
+    // Now clear the cache.
+    _menu_clear_page_cache();
+  }
+
+  /**
+   * Sets an internal flag that allows us to prevent the reparenting operations
+   * executed during deletion.
+   *
+   * @param bool $value
+   */
+  public function preventReparenting($value = FALSE) {
+    $this->preventReparenting = $value;
+  }
+
+  /**
+   * Overrides Drupal\entity\DatabaseStorageController::preDelete().
+   */
+  protected function preDelete($entities) {
+    // Nothing to do if we don't want to reparent children.
+    if ($this->preventReparenting) {
+      return;
+    }
+
+    foreach ($entities as $entity) {
+      // Children get re-attached to the item's parent.
+      if ($entity->has_children) {
+        $children = $this->loadByProperties(array('plid' => $entity->plid));
+        foreach ($children as $child) {
+          $child->plid = $entity->plid;
+          $this->save($child);
+        }
+      }
+    }
+  }
+
+  /**
+   * Overrides Drupal\entity\DatabaseStorageController::postDelete().
+   */
+  protected function postDelete($entities) {
+    $affected_menus = array();
+    // Update the has_children status of the parent.
+    foreach ($entities as $entity) {
+      if (!$this->preventReparenting) {
+        $this->updateParentalStatus($entity);
+      }
+
+      // Store all menu names for which we need to clear the cache.
+      if (!isset($affected_menus[$entity->menu_name])) {
+        $affected_menus[$entity->menu_name] = $entity->menu_name;
+      }
+    }
+
+    foreach ($affected_menus as $menu_name) {
+      menu_cache_clear($menu_name);
+    }
+    _menu_clear_page_cache();
+  }
+
+  /**
+   * Loads updated and customized menu links for specific router paths.
+   *
+   * Note that this is a low-level method and it doesn't return fully populated
+   * menu link entities. (e.g. no fields are attached)
+   *
+   * @param array $router_paths
+   *   An array of router paths.
+   *
+   * @return array
+   *   An array of menu link objects indexed by their ids.
+   */
+  public function loadUpdatedCustomized(array $router_paths) {
+    $query = parent::buildQuery(NULL);
+    $query
+      ->condition(db_or()
+      ->condition('updated', 1)
+      ->condition(db_and()
+        ->condition('router_path', $router_paths, 'NOT IN')
+        ->condition('external', 0)
+        ->condition('customized', 1)
+        )
+      );
+    $query_result = $query->execute();
+
+    if (!empty($this->entityInfo['class'])) {
+      // We provide the necessary arguments for PDO to create objects of the
+      // specified entity class.
+      // @see Drupal\Core\Entity\EntityInterface::__construct()
+      $query_result->setFetchMode(\PDO::FETCH_CLASS, $this->entityInfo['class'], array(array(), $this->entityType));
+    }
+
+    return $query_result->fetchAllAssoc($this->idKey);
+  }
+
+  /**
+   * Loads system menu link as needed by system_get_module_admin_tasks().
+   *
+   * @return array<MenuLink>
+   *   An array of menu link entities indexed by their IDs.
+   */
+  public function loadModuleAdminTasks() {
+    $query = $this->buildQuery(NULL);
+    $query
+      ->condition('base.link_path', 'admin/%', 'LIKE')
+      ->condition('base.hidden', 0, '>=')
+      ->condition('base.module', 'system')
+      ->condition('m.number_parts', 1, '>')
+      ->condition('m.page_callback', 'system_admin_menu_block_page', '<>');
+    $ids = $query->execute()->fetchCol(1);
+
+    return $this->load($ids);
+  }
+
+  /**
+   * Checks and updates the 'has_children' property for the parent of a link.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   A menu link entity.
+   */
+  protected function updateParentalStatus(EntityInterface $entity, $exclude = FALSE) {
+    // If plid == 0, there is nothing to update.
+    if ($entity->plid && ($parent_entity = $this->load(array($entity->plid)))) {
+      // Check if at least one visible child exists in the table.
+      $query = entity_query($this->entityType);
+      $query
+        ->condition('menu_name', $entity->menu_name)
+        ->condition('hidden', 0)
+        ->condition('plid', $entity->plid)
+        ->count();
+
+      if ($exclude) {
+        $query->condition('mlid', $entity->id(), '<>');
+      }
+
+      $parent_has_children = ((bool) $query->execute()) ? 1 : 0;
+      $parent_entity = reset($parent_entity);
+      $parent_entity->has_children = $parent_has_children;
+      $parent_entity->save();
+    }
+  }
+
+  /**
+   * Finds a possible parent for a given menu link entity.
+   *
+   * Because the parent of a given link might not exist anymore in the database,
+   * we apply a set of heuristics to determine a proper parent:
+   *
+   *  - use the passed parent link if specified and existing.
+   *  - else, use the first existing link down the previous link hierarchy
+   *  - else, for system menu links (derived from hook_menu()), reparent
+   *    based on the path hierarchy.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   A menu link entity.
+   * @param array $parent_candidates
+   *   An array of menu link entities keyed by mlid.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface|false
+   *   A menu link entity structure of the possible parent or FALSE if no valid
+   *   parent has been found.
+   */
+  protected function findParent(EntityInterface $entity, array $parent_candidates = array()) {
+    $parent = FALSE;
+
+    // This item is explicitely top-level, skip the rest of the parenting.
+    if (isset($entity->plid) && empty($entity->plid)) {
+      return $parent;
+    }
+
+    // If we have a parent link ID, try to use that.
+    $candidates = array();
+    if (isset($entity->plid)) {
+      $candidates[] = $entity->plid;
+    }
+
+    // Else, if we have a link hierarchy try to find a valid parent in there.
+    if (!empty($entity->depth) && $entity->depth > 1) {
+      for ($depth = $entity->depth - 1; $depth >= 1; $depth--) {
+        $parent_property = "p$depth";
+        $candidates[] = $entity->$parent_property;
+      }
+    }
+
+    foreach ($candidates as $mlid) {
+      if (isset($parent_candidates[$mlid])) {
+        $parent = $parent_candidates[$mlid];
+      }
+      else {
+        $parent = $this->load(array($mlid));
+        $parent = reset($parent);
+      }
+      if ($parent) {
+        return $parent;
+      }
+    }
+
+    // If everything else failed, try to derive the parent from the path
+    // hierarchy. This only makes sense for links derived from menu router
+    // items (ie. from hook_menu()).
+    if ($entity->module == 'system') {
+      // Find the parent - it must be unique.
+      $parent_path = $entity->link_path;
+      do {
+        $parent = FALSE;
+        $parent_path = substr($parent_path, 0, strrpos($parent_path, '/'));
+
+        // @todo Return to the previous method of cloning the entity query when
+        // http://drupal.org/node/1829942 is fixed.
+        $query = entity_query($this->entityType);
+        $query
+          ->condition('mlid', $entity->id(), '<>')
+          ->condition('module', 'system')
+          // We always respect the link's 'menu_name'; inheritance for router
+          // items is ensured in _menu_router_build().
+          ->condition('menu_name', $entity->menu_name)
+          ->condition('link_path', $parent_path);
+
+        $count_query = clone $query;
+        // Only valid if we get a unique result.
+        if ($count_query->count()->execute() == 1) {
+          $result = $query->execute();
+          $parent = $this->load($result);
+          $parent = reset($parent);
+        }
+      } while ($parent === FALSE && $parent_path);
+    }
+
+    return $parent;
+  }
+
+  /**
+   * Sets the p1 through p9 properties for a menu link entity being saved.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   A menu link entity.
+   * @param \Drupal\Core\Entity\EntityInterface $parent
+   *   A menu link entity.
+   */
+  protected function setParents(EntityInterface $entity, EntityInterface $parent) {
+    $i = 1;
+    while ($i < $entity->depth) {
+      $p = 'p' . $i++;
+      $entity->{$p} = $parent->{$p};
+    }
+    $p = 'p' . $i++;
+    // The parent (p1 - p9) corresponding to the depth always equals the mlid.
+    $entity->{$p} = $entity->id();
+    while ($i <= MENU_MAX_DEPTH) {
+      $p = 'p' . $i++;
+      $entity->{$p} = 0;
+    }
+  }
+
+  /**
+   * Finds the depth of an item's children relative to its depth.
+   *
+   * For example, if the item has a depth of 2 and the maximum of any child in
+   * the menu link tree is 5, the relative depth is 3.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   A menu link entity.
+   *
+   * @return int
+   *   The relative depth, or zero.
+   */
+  public function findChildrenRelativeDepth(EntityInterface $entity) {
+    // @todo Since all we need is a specific field from the base table, does it
+    // make sense to convert to EFQ?
+    $query = db_select('menu_links');
+    $query->addField('menu_links', 'depth');
+    $query->condition('menu_name', $entity->menu_name);
+    $query->orderBy('depth', 'DESC');
+    $query->range(0, 1);
+
+    $i = 1;
+    $p = 'p1';
+    while ($i <= MENU_MAX_DEPTH && $entity->{$p}) {
+      $query->condition($p, $entity->{$p});
+      $p = 'p' . ++$i;
+    }
+
+    $max_depth = $query->execute()->fetchField();
+
+    return ($max_depth > $entity->depth) ? $max_depth - $entity->depth : 0;
+  }
+
+  /**
+   * Updates the children of a menu link that is being moved.
+   *
+   * The menu name, parents (p1 - p6), and depth are updated for all children of
+   * the link, and the has_children status of the previous parent is updated.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   A menu link entity.
+   */
+  protected function moveChildren(EntityInterface $entity) {
+    $query = db_update($this->entityInfo['base_table']);
+
+    $query->fields(array('menu_name' => $entity->menu_name));
+
+    $p = 'p1';
+    $expressions = array();
+    for ($i = 1; $i <= $entity->depth; $p = 'p' . ++$i) {
+      $expressions[] = array($p, ":p_$i", array(":p_$i" => $entity->{$p}));
+    }
+    $j = $entity->original->depth + 1;
+    while ($i <= MENU_MAX_DEPTH && $j <= MENU_MAX_DEPTH) {
+      $expressions[] = array('p' . $i++, 'p' . $j++, array());
+    }
+    while ($i <= MENU_MAX_DEPTH) {
+      $expressions[] = array('p' . $i++, 0, array());
+    }
+
+    $shift = $entity->depth - $entity->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', $entity->original->menu_name);
+    $p = 'p1';
+    for ($i = 1; $i <= MENU_MAX_DEPTH && $entity->original->{$p}; $p = 'p' . ++$i) {
+      $query->condition($p, $entity->original->{$p});
+    }
+
+    $query->execute();
+
+    // Check the has_children status of the parent, while excluding this item.
+    $this->updateParentalStatus($entity->original, TRUE);
+  }
+
+  /**
+   * Returns the number of menu links from a menu.
+   *
+   * @param string $menu_name
+   *   The unique name of a menu.
+   */
+  public function countMenuLinks($menu_name) {
+    $query = entity_query($this->entityType);
+    $query
+      ->condition('menu_name', $menu_name)
+      ->count();
+    return $query->execute();
+  }
+}
diff --git a/core/modules/menu_link/lib/Drupal/menu_link/Plugin/Core/Entity/MenuLink.php b/core/modules/menu_link/lib/Drupal/menu_link/Plugin/Core/Entity/MenuLink.php
new file mode 100644
index 0000000..106fb0c
--- /dev/null
+++ b/core/modules/menu_link/lib/Drupal/menu_link/Plugin/Core/Entity/MenuLink.php
@@ -0,0 +1,285 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_link\Plugin\Core\Entity\MenuLink.
+ */
+
+namespace Drupal\menu_link\Plugin\Core\Entity;
+
+use Drupal\Core\Annotation\Plugin;
+use Drupal\Core\Annotation\Translation;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\Entity;
+
+/**
+ * Defines the menu link entity class.
+ *
+ * @Plugin(
+ *   id = "menu_link",
+ *   label = @Translation("Menu link"),
+ *   module = "menu_link",
+ *   controller_class = "Drupal\menu_link\MenuLinkStorageController",
+ *   form_controller_class = {
+ *     "default" = "Drupal\menu_link\MenuLinkFormController"
+ *   },
+ *   base_table = "menu_links",
+ *   uri_callback = "menu_link_uri",
+ *   entity_keys = {
+ *     "id" = "mlid",
+ *     "label" = "link_title",
+ *     "uuid" = "uuid"
+ *   },
+ *   bundles = {
+ *     "menu_link" = {
+ *       "label" = "Menu link",
+ *     }
+ *   }
+ * )
+ */
+class MenuLink extends Entity implements \ArrayAccess, ContentEntityInterface {
+
+  /**
+   * The link's menu name.
+   *
+   * @var string
+   */
+  public $menu_name = 'tools';
+
+  /**
+   * The menu link ID.
+   *
+   * @var integer
+   */
+  public $mlid;
+
+  /**
+   * The menu link UUID.
+   *
+   * @var string
+   */
+  public $uuid;
+
+  /**
+   * The parent link ID.
+   *
+   * @var integer
+   */
+  public $plid;
+
+  /**
+   * The Drupal path or external path this link points to.
+   *
+   * @var string
+   */
+  public $link_path;
+
+  /**
+   * For links corresponding to a Drupal path (external = 0), this connects the
+   * link to a {menu_router}.path for joins.
+   *
+   * @var string
+   */
+  public $router_path;
+
+  /**
+   * @var string
+   */
+  public $link_title = '';
+
+  /**
+   * @var array
+   */
+  public $options = array();
+
+  /**
+   * @var string
+   */
+  public $module = 'menu';
+
+  /**
+   * @var integer
+   */
+  public $hidden = 0;
+
+  /**
+   * @var integer
+   */
+  public $external;
+
+  /**
+   * @var integer
+   */
+  public $has_children = 0;
+
+  /**
+   * @var integer
+   */
+  public $expanded = 0;
+
+  /**
+   * @var integer
+   */
+  public $weight = 0;
+
+  /**
+   * @var integer
+   */
+  public $depth;
+
+  /**
+   * @var integer
+   */
+  public $customized = 0;
+
+  /**
+   * @var integer
+   *
+   * @todo Investigate whether the p1, p2, .. pX properties can be moved to a
+   * single array property.
+   */
+  public $p1;
+
+  /**
+   * @var integer
+   */
+  public $p2;
+
+  /**
+   * @var integer
+   */
+  public $p3;
+
+  /**
+   * @var integer
+   */
+  public $p4;
+
+  /**
+   * @var integer
+   */
+  public $p5;
+
+  /**
+   * @var integer
+   */
+  public $p6;
+
+  /**
+   * @var integer
+   */
+  public $p7;
+
+  /**
+   * @var integer
+   */
+  public $p8;
+
+  /**
+   * @var integer
+   */
+  public $p9;
+
+  /**
+   * The menu link modification timestamp.
+   *
+   * @var integer
+   */
+  public $updated = 0;
+
+  /**
+   * Overrides Drupal\Entity\Entity::id().
+   */
+  public function id() {
+    return $this->mlid;
+  }
+
+  /**
+   * Overrides Drupal\entity\Entity::createDuplicate().
+   */
+  public function createDuplicate() {
+    $duplicate = parent::createDuplicate();
+    $duplicate->plid = NULL;
+    return $duplicate;
+  }
+
+  /**
+   * Resets a system-defined menu link.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   *   A menu link entity.
+   */
+  public function reset() {
+    // To reset the link to its original values, we need to retrieve its
+    // definition from hook_menu(). Otherwise, for example, the link's menu
+    // would not be reset, because properties like the original 'menu_name' are
+    // not stored anywhere else. Since resetting a link happens rarely and this
+    // is a one-time operation, retrieving the full menu router does no harm.
+    $menu = menu_get_router();
+    $router_item = $menu[$this->router_path];
+    $new_link = self::buildFromRouterItem($router_item);
+    // Merge existing menu link's ID and 'has_children' property.
+    foreach (array('mlid', 'has_children') as $key) {
+      $new_link->{$key} = $this->{$key};
+    }
+    $new_link->save();
+    return $new_link;
+  }
+
+  /**
+   * Builds a menu link entity from a router item.
+   *
+   * @param array $item
+   *   A menu router item.
+   *
+   * @return MenuLink
+   *   A menu link entity.
+   */
+  public static function buildFromRouterItem(array $item) {
+    // Suggested items are disabled by default.
+    if ($item['type'] == MENU_SUGGESTED_ITEM) {
+      $item['hidden'] = 1;
+    }
+    // Hide all items that are not visible in the tree.
+    elseif (!($item['type'] & MENU_VISIBLE_IN_TREE)) {
+      $item['hidden'] = -1;
+    }
+    // Note, we set this as 'system', so that we can be sure to distinguish all
+    // the menu links generated automatically from entries in {menu_router}.
+    $item['module'] = 'system';
+    $item += array(
+      'link_title' => $item['title'],
+      'link_path' => $item['path'],
+      'options' => empty($item['description']) ? array() : array('attributes' => array('title' => $item['description'])),
+    );
+    return entity_get_controller('menu_link')->create($item);
+  }
+
+  /**
+   * Implements ArrayAccess::offsetExists().
+   */
+  public function offsetExists($offset) {
+    return isset($this->{$offset});
+  }
+
+  /**
+   * Implements ArrayAccess::offsetGet().
+   */
+  public function &offsetGet($offset) {
+    return $this->{$offset};
+  }
+
+  /**
+   * Implements ArrayAccess::offsetSet().
+   */
+  public function offsetSet($offset, $value) {
+    $this->{$offset} = $value;
+  }
+
+  /**
+   * Implements ArrayAccess::offsetUnset().
+   */
+  public function offsetUnset($offset) {
+    unset($this->{$offset});
+  }
+}
diff --git a/core/modules/menu_link/menu_link.api.php b/core/modules/menu_link/menu_link.api.php
new file mode 100644
index 0000000..0899cbf
--- /dev/null
+++ b/core/modules/menu_link/menu_link.api.php
@@ -0,0 +1,138 @@
+<?php
+
+/**
+ * @file
+ * Hooks provided by the Menu link module.
+ */
+
+/**
+ * @addtogroup hooks
+ * @{
+ */
+
+/**
+ * Alter menu links when loaded and before they are rendered.
+ *
+ * This hook is only invoked if $menu_link->options['alter'] has been set to a
+ * non-empty value (e.g., TRUE). This flag should be set using
+ * hook_menu_link_presave().
+ * @ todo The paragraph above is lying! This hasn't been (re)implemented yet.
+ *
+ * Implementations of this hook are able to alter any property of the menu link.
+ * For example, this hook may be used to add a page-specific query string to all
+ * menu links, or hide a certain link by setting:
+ * @code
+ *   'hidden' => 1,
+ * @endcode
+ *
+ * @param array $menu_links
+ *   An array of menu link entities.
+ *
+ * @see hook_menu_link_presave()
+ */
+function hook_menu_link_load($menu_links) {
+  foreach ($menu_links as $menu_link) {
+    if ($menu_link->href == 'devel/cache/clear') {
+      $menu_link->options['query'] = drupal_get_destination();
+    }
+  }
+}
+
+
+/**
+ * Alter the data of a menu link entity before it is created or updated.
+ *
+ * @param \Drupal\menu_link\Plugin\Core\Entity\MenuLink $menu_link
+ *   A menu link entity.
+ *
+ * @see hook_menu_link_load()
+ */
+function hook_menu_link_presave($menu_link) {
+  // Make all new admin links hidden (a.k.a disabled).
+  if (strpos($menu_link->link_path, 'admin') === 0 && $menu_link->isNew()) {
+    $menu_link->hidden = 1;
+  }
+  // Flag a link to be altered by hook_menu_link_load().
+  if ($menu_link->link_path == 'devel/cache/clear') {
+    $menu_link->options['alter'] = TRUE;
+  }
+  // Flag a menu link to be altered by hook_menu_link_load(), but only if it is
+  // derived from a menu router item; i.e., do not alter a custom menu link
+  // pointing to the same path that has been created by a user.
+  if ($menu_link->link_path == 'user' && $menu_link->module == 'system') {
+    $menu_link->options['alter'] = TRUE;
+  }
+}
+
+/**
+ * Inform modules that a menu link has been created.
+ *
+ * This hook is used to notify modules that menu links have been
+ * created. Contributed modules may use the information to perform
+ * actions based on the information entered into the menu system.
+ *
+ * @param \Drupal\menu_link\Plugin\Core\Entity\MenuLink $menu_link
+ *   A menu link entity.
+ *
+ * @see hook_menu_link_presave()
+ * @see hook_menu_link_update()
+ * @see hook_menu_link_delete()
+ */
+function hook_menu_link_insert($menu_link) {
+  // In our sample case, we track menu items as editing sections
+  // of the site. These are stored in our table as 'disabled' items.
+  $record['mlid'] = $menu_link->id();
+  $record['menu_name'] = $menu_link->menu_name;
+  $record['status'] = 0;
+  drupal_write_record('menu_example', $record);
+}
+
+/**
+ * Inform modules that a menu link has been updated.
+ *
+ * This hook is used to notify modules that menu items have been
+ * updated. Contributed modules may use the information to perform
+ * actions based on the information entered into the menu system.
+ *
+ * @param \Drupal\menu_link\Plugin\Core\Entity\MenuLink $menu_link
+ *   A menu link entity.
+ *
+ * @see hook_menu_link_presave()
+ * @see hook_menu_link_insert()
+ * @see hook_menu_link_delete()
+ */
+function hook_menu_link_update($menu_link) {
+  // If the parent menu has changed, update our record.
+  $menu_name = db_query("SELECT menu_name FROM {menu_example} WHERE mlid = :mlid", array(':mlid' => $menu_link->id()))->fetchField();
+  if ($menu_name != $menu_link->menu_name) {
+    db_update('menu_example')
+      ->fields(array('menu_name' => $menu_link->menu_name))
+      ->condition('mlid', $menu_link->id())
+      ->execute();
+  }
+}
+
+/**
+ * Inform modules that a menu link has been deleted.
+ *
+ * This hook is used to notify modules that menu links have been
+ * deleted. Contributed modules may use the information to perform
+ * actions based on the information entered into the menu system.
+ *
+ * @param \Drupal\menu_link\Plugin\Core\Entity\MenuLink $menu_link
+ *   A menu link entity.
+ *
+ * @see hook_menu_link_presave()
+ * @see hook_menu_link_insert()
+ * @see hook_menu_link_update()
+ */
+function hook_menu_link_delete($menu_link) {
+  // Delete the record from our table.
+  db_delete('menu_example')
+    ->condition('mlid', $menu_link->id())
+    ->execute();
+}
+
+/**
+ * @} End of "addtogroup hooks".
+ */
diff --git a/core/modules/menu_link/menu_link.info b/core/modules/menu_link/menu_link.info
new file mode 100644
index 0000000..c5e92d7
--- /dev/null
+++ b/core/modules/menu_link/menu_link.info
@@ -0,0 +1,8 @@
+name = Menu Link
+description = Provides menu links, trees and bunnies!
+package = Core
+version = VERSION
+core = 8.x
+; @todo Menu links functionality has been moved from system.module and menu.inc
+; to this module, so make it required until everything is moved over.
+required = TRUE
diff --git a/core/modules/menu_link/menu_link.install b/core/modules/menu_link/menu_link.install
new file mode 100644
index 0000000..d45314a
--- /dev/null
+++ b/core/modules/menu_link/menu_link.install
@@ -0,0 +1,213 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the menu_link module.
+ */
+
+/**
+ * Implements hook_schema().
+ */
+function menu_link_schema() {
+  $schema['menu_links'] = array(
+    'description' => 'Contains the individual links within a menu.',
+    '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,
+      ),
+      'uuid' => array(
+        'description' => 'Unique Key: Universally unique identifier for this entity.',
+        'type' => 'varchar',
+        'length' => 128,
+        'not null' => FALSE,
+      ),
+      'plid' => array(
+        'description' => 'The parent link ID (plid) is the mlid of the link above in the hierarchy, or zero if the link is at the top level in its menu.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'link_path' => array(
+        'description' => 'The Drupal path or external path this link points to.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'router_path' => array(
+        'description' => 'For links corresponding to a Drupal path (external = 0), this connects the link to a {menu_router}.path for joins.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'langcode' => array(
+        'description' => 'The {language}.langcode of this link.',
+        'type' => 'varchar',
+        'length' => 12,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'link_title' => array(
+        'description' => 'The text displayed for the link, which may be modified by a title callback stored in {menu_router}.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+        'translatable' => TRUE,
+      ),
+      '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',
+        'not null' => FALSE,
+        'translatable' => TRUE,
+        'serialize' => TRUE,
+      ),
+      'module' => array(
+        'description' => 'The name of the module that generated this link.',
+        'type' => 'varchar',
+        'length' => 255,
+        '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, -1 = a menu callback, 0 = a normal, visible link)',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+        'size' => 'small',
+      ),
+      'external' => array(
+        'description' => 'A flag to indicate if the link points to a full URL starting with a protocol, like http:// (1 = external, 0 = internal).',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+        'size' => 'small',
+      ),
+      'has_children' => array(
+        'description' => 'Flag indicating whether any links have this link as a parent (1 = children exist, 0 = no children).',
+        '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,
+      ),
+      'depth' => array(
+        'description' => 'The depth relative to the top level. A link with plid == 0 will have depth == 1.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+        'size' => 'small',
+      ),
+      'customized' => array(
+        'description' => 'A flag to indicate that the user has manually created or edited the link (1 = customized, 0 = not customized).',
+        '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 plid. 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,
+      ),
+      'updated' => array(
+        'description' => 'Flag that indicates that this link was generated during the update from Drupal 5.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+        'size' => 'small',
+      ),
+    ),
+    'indexes' => array(
+      'path_menu' => array(array('link_path', 128), 'menu_name'),
+      'menu_plid_expand_child' => array('menu_name', 'plid', 'expanded', 'has_children'),
+      'menu_parents' => array('menu_name', 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9'),
+      'router_path' => array(array('router_path', 128)),
+    ),
+    'primary key' => array('mlid'),
+  );
+
+  return $schema;
+}
diff --git a/core/modules/menu_link/menu_link.module b/core/modules/menu_link/menu_link.module
new file mode 100644
index 0000000..5553d11
--- /dev/null
+++ b/core/modules/menu_link/menu_link.module
@@ -0,0 +1,206 @@
+<?php
+
+/**
+ * @file
+ * Enables users to create menu links.
+ */
+
+use Drupal\menu_link\Plugin\Core\Entity\MenuLink;
+
+/**
+ * Entity URI callback.
+ *
+ * @param \Drupal\menu_link\Plugin\Core\Entity\MenuLink $menu_link
+ *   A menu link entity.
+ */
+function menu_link_uri(MenuLink $menu_link) {
+  return array(
+    'path' => $menu_link->link_path,
+  );
+}
+
+/**
+ * Loads a menu link entity.
+ *
+ * This function should never be called from within node_load() or any other
+ * function used as a menu object load function since an infinite recursion may
+ * occur.
+ *
+ * @param int $mlid
+ *   The menu link ID.
+ * @param bool $reset
+ *   (optional) Whether to reset the menu_link_load_multiple() cache.
+ *
+ * @return \Drupal\menu_link\Plugin\Core\Entity\MenuLink|false
+ *   A menu link entity.
+ */
+function menu_link_load($mlid = NULL, $reset = FALSE) {
+  return entity_load('menu_link', $mlid, $reset);
+}
+
+/**
+ * Loads menu link entities from the database.
+ *
+ * @param array $mlids
+ *   (optional) An array of entity IDs. If omitted, all entities are loaded.
+ * @param bool $reset
+ *   (optional) Whether to reset the internal cache.
+ *
+ * @return array<\Drupal\menu_link\Plugin\Core\Entity\MenuLink>
+ *   An array of menu link entities indexed by mlid.
+ *
+ * @see menu_link_load()
+ * @see entity_load_multiple()
+ */
+function menu_link_load_multiple(array $mlids = NULL, $reset = FALSE) {
+  return entity_load_multiple('menu_link', $mlids, $reset);
+}
+
+/**
+ * Deletes a menu link.
+ *
+ * @param int $mlid
+ *   The menu link ID.
+ *
+ * @see menu_link_delete_multiple()
+ */
+function menu_link_delete($mlid) {
+  menu_link_delete_multiple(array($mlid));
+}
+
+/**
+ * Deletes multiple menu links.
+ *
+ * @param array $mlids
+ *   An array of menu link IDs.
+ * @param bool $force
+ *   (optional) Forces deletion. Internal use only, setting to TRUE is
+ *   discouraged. Defaults to FALSE.
+ * @param bool $prevent_reparenting
+ *   (optional) Disables the re-parenting logic from the deletion process.
+ *   Defaults to FALSE.
+ */
+function menu_link_delete_multiple(array $mlids, $force = FALSE, $prevent_reparenting = FALSE) {
+  if (!$mlids) {
+    // If no IDs or invalid IDs were passed, do nothing.
+    return;
+  }
+
+  $controller = entity_get_controller('menu_link');
+  if (!$force) {
+    $entity_query = entity_query('menu_link');
+    $group = $entity_query->orConditionGroup()
+      ->condition('module', 'system', '<>')
+      ->condition('updated', 0, '<>');
+
+    $entity_query->condition('mlid', $mlids, 'IN');
+    $entity_query->condition($group);
+
+    $result = $entity_query->execute();
+    $entities = $controller->load($result);
+  }
+  else {
+    $entities = $controller->load($mlids);
+  }
+  $controller->preventReparenting($prevent_reparenting);
+  $controller->delete($entities);
+}
+
+/**
+ * Saves a menu link.
+ *
+ * After calling this function, rebuild the menu cache using
+ * menu_cache_clear_all().
+ *
+ * @param \Drupal\menu_link\Plugin\Core\Entity\MenuLink $menu_link
+ *   The menu link entity to be saved.
+ */
+function menu_link_save(MenuLink $menu_link) {
+  $menu_link->save();
+}
+
+/**
+ * Clones an array of menu links.
+ *
+ * @param array<MenuLink> $links
+ *   An array of menu links to clone.
+ * @param string $menu_name
+ *   (optional) The name of a menu that the links will be cloned for. If not
+ *   set, the cloned links will be in the same menu as the original set of
+ *   links that were passed in.
+ *
+ * @return array<MenuLink>
+ *   An array of menu links with the same properties as the passed-in array,
+ *   but with the link identifiers removed so that a new link will be created
+ *   when any of them is passed into
+ *   Drupal\menu_link\MenuLinkStorageController::save().
+ *
+ * @see Drupal\menu_link\MenuLinkStorageController::save()
+ */
+function menu_link_clone($links, $menu_name = NULL) {
+  foreach ($links as &$link) {
+    $link = $link->createDuplicate();
+    if (isset($menu_name)) {
+      $link->menu_name = $menu_name;
+    }
+  }
+  return $links;
+}
+
+/**
+ * Inserts, updates, enables, disables, or deletes an uncustomized menu link.
+ *
+ * @param string $module
+ *   The name of the module that owns the link.
+ * @param string $op
+ *   Operation to perform: insert, update, enable, disable, or delete.
+ * @param string $link_path
+ *   The path this link points to.
+ * @param string $link_title
+ *   (optional) Title of the link to insert or new title to update the link to.
+ *   Unused for delete. Defaults to NULL.
+ *
+ * @return integer|null
+ *   The insert op returns the mlid of the new item. Others op return NULL.
+ */
+function menu_link_maintain($module, $op, $link_path, $link_title = NULL) {
+  switch ($op) {
+    case 'insert':
+      $menu_link = entity_create('menu_link', array(
+        'link_title' => $link_title,
+        'link_path' => $link_path,
+        'module' => $module,)
+      );
+      return $menu_link->save();
+
+    case 'update':
+      $menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $link_path, 'module' => $module, 'customized' => 0));
+      foreach ($menu_links as $menu_link) {
+        $menu_link->original = $menu_link;
+        if (isset($link_title)) {
+          $menu_link->link_title = $link_title;
+        }
+        $menu_link->save();
+      }
+      break;
+
+    case 'enable':
+    case 'disable':
+      $menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $link_path, 'module' => $module, 'customized' => 0));
+      foreach ($menu_links as $menu_link) {
+        $menu_link->original = $menu_link;
+        $menu_link->hidden = ($op == 'disable' ? 1 : 0);
+        $menu_link->customized = 1;
+        if (isset($link_title)) {
+          $menu_link->link_title = $link_title;
+        }
+        $menu_link->save();
+      }
+      break;
+
+    case 'delete':
+      $menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $link_path));
+      menu_link_delete_multiple(array_keys($menu_links));
+      break;
+  }
+}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php b/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php
index 8c94435..57c5d9e 100644
--- a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php
+++ b/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php
@@ -52,16 +52,16 @@ function setUp() {
 
       // Populate the default shortcut set.
       $shortcut_set = shortcut_set_load(SHORTCUT_DEFAULT_SET_NAME);
-      $shortcut_set->links[] = array(
+      $shortcut_set->links[] = entity_create('menu_link', array(
         'link_path' => 'node/add',
         'link_title' => st('Add content'),
         'weight' => -20,
-      );
-      $shortcut_set->links[] = array(
+      ));
+      $shortcut_set->links[] = entity_create('menu_link', array(
         'link_path' => 'admin/content',
         'link_title' => st('Find content'),
         'weight' => -19,
-      );
+      ));
       shortcut_set_save($shortcut_set);
     }
 
@@ -75,6 +75,8 @@ function setUp() {
     // Log in as admin and grab the default shortcut set.
     $this->drupalLogin($this->admin_user);
     $this->set = shortcut_set_load(SHORTCUT_DEFAULT_SET_NAME);
+    // Reset the keys of the links array.
+    $this->set->links = array_values($this->set->links);
     shortcut_set_assign_user($this->set, $this->admin_user);
   }
 
@@ -104,10 +106,10 @@ function generateShortcutSet($title = '', $default_links = TRUE, $set_name = '')
    * Creates a generic shortcut link.
    */
   function generateShortcutLink($path, $title = '') {
-    $link = array(
+    $link = entity_create('menu_link', array(
       'link_path' => $path,
       'link_title' => !empty($title) ? $title : $this->randomName(10),
-    );
+    ));
 
     return $link;
   }
diff --git a/core/modules/shortcut/shortcut.admin.inc b/core/modules/shortcut/shortcut.admin.inc
index 18ec1c3..017843e 100644
--- a/core/modules/shortcut/shortcut.admin.inc
+++ b/core/modules/shortcut/shortcut.admin.inc
@@ -120,7 +120,7 @@ function shortcut_set_switch_submit($form, &$form_state) {
     $default_set = shortcut_default_set($account);
     $set = (object) array(
       'title' => $form_state['values']['new'],
-      'links' => menu_links_clone($default_set->links),
+      'links' => menu_link_clone($default_set->links),
     );
     shortcut_set_save($set);
     $replacements = array(
@@ -241,7 +241,7 @@ function shortcut_set_add_form_submit($form, &$form_state) {
   $default_set = shortcut_default_set();
   $set = (object) array(
     'title' => $form_state['values']['new'],
-    'links' => menu_links_clone($default_set->links),
+    'links' => menu_link_clone($default_set->links),
   );
   shortcut_set_save($set);
   drupal_set_message(t('The %set_name shortcut set has been created. You can edit it from this page.', array('%set_name' => $set->title)));
@@ -424,10 +424,10 @@ function shortcut_link_edit($form, &$form_state, $shortcut_link) {
  */
 function _shortcut_link_form_elements($shortcut_link = NULL) {
   if (!isset($shortcut_link)) {
-    $shortcut_link = array(
+    $shortcut_link = entity_create('menu_link', array(
       'link_title' => '',
       'link_path' => ''
-    );
+    ));
   }
   else {
     $shortcut_link['link_path'] = ($shortcut_link['link_path'] == '<front>') ? '' : drupal_container()->get('path.alias_manager')->getPathAlias($shortcut_link['link_path']);
@@ -483,7 +483,10 @@ function shortcut_link_edit_submit($form, &$form_state) {
   }
   $form_state['values']['shortcut_link']['link_path'] = $shortcut_path;
 
-  $shortcut_link = array_merge($form_state['values']['original_shortcut_link'], $form_state['values']['shortcut_link']);
+  $shortcut_link = $form_state['values']['original_shortcut_link'];
+  foreach ($form_state['values']['shortcut_link'] as $key => $value) {
+   $shortcut_link[$key] = $value;
+  }
 
   menu_link_save($shortcut_link);
   $form_state['redirect'] = 'admin/config/user-interface/shortcut/' . $shortcut_link['menu_name'];
@@ -496,7 +499,7 @@ function shortcut_link_edit_submit($form, &$form_state) {
 function shortcut_link_add_submit($form, &$form_state) {
   // Add the shortcut link to the set.
   $shortcut_set = $form_state['values']['shortcut_set'];
-  $shortcut_link = $form_state['values']['shortcut_link'];
+  $shortcut_link = entity_create('menu_link', $form_state['values']['shortcut_link']);
   $shortcut_link['menu_name'] = $shortcut_set->set_name;
   shortcut_admin_add_link($shortcut_link, $shortcut_set);
   shortcut_set_save($shortcut_set);
diff --git a/core/modules/shortcut/shortcut.info b/core/modules/shortcut/shortcut.info
index 5ed5f2d..97f25c6 100644
--- a/core/modules/shortcut/shortcut.info
+++ b/core/modules/shortcut/shortcut.info
@@ -3,4 +3,5 @@ description = Allows users to manage customizable lists of shortcut links.
 package = Core
 version = VERSION
 core = 8.x
+dependencies[] = menu_link
 configure = admin/config/user-interface/shortcut
diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/BreadcrumbTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/BreadcrumbTest.php
index a77218b..1016b18 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Menu/BreadcrumbTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Menu/BreadcrumbTest.php
@@ -207,13 +207,13 @@ function testBreadCrumbs() {
       $node2 = $this->drupalCreateNode(array(
         'type' => $type,
         'title' => $title,
-        'menu' => array(
+        'menu' => entity_create('menu_link', array(
           'enabled' => 1,
           'link_title' => 'Parent ' . $title,
           'description' => '',
           'menu_name' => $menu,
           'plid' => 0,
-        ),
+        )),
       ));
       $nid2 = $node2->nid;
 
@@ -235,13 +235,13 @@ function testBreadCrumbs() {
       $node3 = $this->drupalCreateNode(array(
         'type' => $type,
         'title' => $title,
-        'menu' => array(
+        'menu' => entity_create('menu_link', array(
           'enabled' => 1,
           'link_title' => 'Child ' . $title,
           'description' => '',
           'menu_name' => $menu,
           'plid' => $node2->menu['mlid'],
-        ),
+        )),
       ));
       $nid3 = $node3->nid;
 
@@ -275,7 +275,8 @@ function testBreadCrumbs() {
       'link_path' => 'node',
     );
     $this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save'));
-    $link = db_query('SELECT * FROM {menu_links} WHERE link_title = :title', array(':title' => 'Root'))->fetchAssoc();
+    $menu_links = entity_load_multiple_by_properties('menu_link', array('link_title' => 'Root'));
+    $link = reset($menu_links);
 
     $edit = array(
       'menu[parent]' => $link['menu_name'] . ':' . $link['mlid'],
@@ -333,10 +334,8 @@ function testBreadCrumbs() {
         'parent' => "$menu:{$parent_mlid}",
       );
       $this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save'));
-      $tags[$name]['link'] = db_query('SELECT * FROM {menu_links} WHERE link_title = :title AND link_path = :href', array(
-        ':title' => $edit['link_title'],
-        ':href' => $edit['link_path'],
-      ))->fetchAssoc();
+      $menu_links = entity_load_multiple_by_properties('menu_link', array('link_title' => $edit['link_title'], 'link_path' => $edit['link_path']));
+      $tags[$name]['link'] = reset($menu_links);
       $tags[$name]['link']['link_path'] = $edit['link_path'];
       $parent_mlid = $tags[$name]['link']['mlid'];
     }
@@ -434,20 +433,16 @@ function testBreadCrumbs() {
       'link_path' => 'user',
     );
     $this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save'));
-    $link_user = db_query('SELECT * FROM {menu_links} WHERE link_title = :title AND link_path = :href', array(
-      ':title' => $edit['link_title'],
-      ':href' => $edit['link_path'],
-    ))->fetchAssoc();
+    $menu_links_user = entity_load_multiple_by_properties('menu_link', array('link_title' => $edit['link_title'], 'link_path' => $edit['link_path']));
+    $link_user = reset($menu_links_user);
 
     $edit = array(
       'link_title' => $this->admin_user->name . ' link',
       'link_path' => 'user/' . $this->admin_user->uid,
     );
     $this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save'));
-    $link_admin_user = db_query('SELECT * FROM {menu_links} WHERE link_title = :title AND link_path = :href', array(
-      ':title' => $edit['link_title'],
-      ':href' => $edit['link_path'],
-    ))->fetchAssoc();
+    $menu_links_admin_user = entity_load_multiple_by_properties('menu_link', array('link_title' => $edit['link_title'], 'link_path' => $edit['link_path']));
+    $link_admin_user = reset($menu_links_admin_user);
 
     // Verify expected breadcrumbs for the two separate links.
     $this->drupalLogout();
diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/LinksTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/LinksTest.php
index 7d22246..ddc834b 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Menu/LinksTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Menu/LinksTest.php
@@ -26,7 +26,8 @@ public static function getInfo() {
    */
   function createLinkHierarchy($module = 'menu_test') {
     // First remove all the menu links.
-    db_truncate('menu_links')->execute();
+    $menu_links = menu_link_load_multiple();
+    menu_link_delete_multiple(array_keys($menu_links), TRUE, TRUE);
 
     // Then create a simple link hierarchy:
     // - $parent
@@ -43,31 +44,36 @@ function createLinkHierarchy($module = 'menu_test') {
     $links['parent'] = $base_options + array(
       'link_path' => 'menu-test/parent',
     );
-    menu_link_save($links['parent']);
+    $links['parent'] = entity_create('menu_link', $links['parent']);
+    $links['parent']->save();
 
     $links['child-1'] = $base_options + array(
       'link_path' => 'menu-test/parent/child-1',
       'plid' => $links['parent']['mlid'],
     );
-    menu_link_save($links['child-1']);
+    $links['child-1'] = entity_create('menu_link', $links['child-1']);
+    $links['child-1']->save();
 
     $links['child-1-1'] = $base_options + array(
       'link_path' => 'menu-test/parent/child-1/child-1-1',
       'plid' => $links['child-1']['mlid'],
     );
-    menu_link_save($links['child-1-1']);
+    $links['child-1-1'] = entity_create('menu_link', $links['child-1-1']);
+    $links['child-1-1']->save();
 
     $links['child-1-2'] = $base_options + array(
       'link_path' => 'menu-test/parent/child-1/child-1-2',
       'plid' => $links['child-1']['mlid'],
     );
-    menu_link_save($links['child-1-2']);
+    $links['child-1-2'] = entity_create('menu_link', $links['child-1-2']);
+    $links['child-1-2']->save();
 
     $links['child-2'] = $base_options + array(
       'link_path' => 'menu-test/parent/child-2',
       'plid' => $links['parent']['mlid'],
     );
-    menu_link_save($links['child-2']);
+    $links['child-2'] = entity_create('menu_link', $links['child-2']);
+    $links['child-2']->save();
 
     return $links;
   }
diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/RouterTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/RouterTest.php
index 179ea93..991ea66 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Menu/RouterTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Menu/RouterTest.php
@@ -304,27 +304,30 @@ function testMenuName() {
     $admin_user = $this->drupalCreateUser(array('administer site configuration'));
     $this->drupalLogin($admin_user);
 
-    $sql = "SELECT menu_name FROM {menu_links} WHERE router_path = 'menu_name_test'";
-    $name = db_query($sql)->fetchField();
-    $this->assertEqual($name, 'original', 'Menu name is "original".');
+    $menu_links = entity_load_multiple_by_properties('menu_link', array('router_path' => 'menu_name_test'));
+    $menu_link = reset($menu_links);
+    $this->assertEqual($menu_link->menu_name, 'original', 'Menu name is "original".');
 
     // Change the menu_name parameter in menu_test.module, then force a menu
     // rebuild.
     menu_test_menu_name('changed');
     menu_router_rebuild();
 
-    $sql = "SELECT menu_name FROM {menu_links} WHERE router_path = 'menu_name_test'";
-    $name = db_query($sql)->fetchField();
-    $this->assertEqual($name, 'changed', 'Menu name was successfully changed after rebuild.');
+    $menu_links = entity_load_multiple_by_properties('menu_link', array('router_path' => 'menu_name_test'));
+    $menu_link = reset($menu_links);
+    $this->assertEqual($menu_link->menu_name, 'changed', 'Menu name was successfully changed after rebuild.');
   }
 
   /**
    * Tests for menu hierarchy.
    */
   function testMenuHierarchy() {
-    $parent_link = db_query('SELECT * FROM {menu_links} WHERE link_path = :link_path', array(':link_path' => 'menu-test/hierarchy/parent'))->fetchAssoc();
-    $child_link = db_query('SELECT * FROM {menu_links} WHERE link_path = :link_path', array(':link_path' => 'menu-test/hierarchy/parent/child'))->fetchAssoc();
-    $unattached_child_link = db_query('SELECT * FROM {menu_links} WHERE link_path = :link_path', array(':link_path' => 'menu-test/hierarchy/parent/child2/child'))->fetchAssoc();
+    $parent_links = entity_load_multiple_by_properties('menu_link', array('link_path' => 'menu-test/hierarchy/parent'));
+    $parent_link = reset($parent_links);
+    $child_links = entity_load_multiple_by_properties('menu_link', array('link_path' => 'menu-test/hierarchy/parent/child'));
+    $child_link = reset($child_links);
+    $unattached_child_links = entity_load_multiple_by_properties('menu_link', array('link_path' => 'menu-test/hierarchy/parent/child2/child'));
+    $unattached_child_link = reset($unattached_child_links);
 
     $this->assertEqual($child_link['plid'], $parent_link['mlid'], 'The parent of a directly attached child is correct.');
     $this->assertEqual($unattached_child_link['plid'], $parent_link['mlid'], 'The parent of a non-directly attached child is correct.');
@@ -335,12 +338,16 @@ function testMenuHierarchy() {
    */
   function testMenuHidden() {
     // Verify links for one dynamic argument.
-    $links = db_select('menu_links', 'ml')
-      ->fields('ml')
-      ->condition('ml.router_path', 'menu-test/hidden/menu%', 'LIKE')
-      ->orderBy('ml.router_path')
-      ->execute()
-      ->fetchAllAssoc('router_path', PDO::FETCH_ASSOC);
+    $query = entity_query('menu_link')
+      ->condition('router_path', 'menu-test/hidden/menu', 'STARTS_WITH')
+      ->sort('router_path');
+    $result = $query->execute();
+    $menu_links = menu_link_load_multiple($result);
+
+    $links = array();
+    foreach ($menu_links as $menu_link) {
+      $links[$menu_link->router_path] = $menu_link;
+    }
 
     $parent = $links['menu-test/hidden/menu'];
     $depth = $parent['depth'] + 1;
@@ -383,12 +390,16 @@ function testMenuHidden() {
     $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid)));
 
     // Verify links for two dynamic arguments.
-    $links = db_select('menu_links', 'ml')
-      ->fields('ml')
-      ->condition('ml.router_path', 'menu-test/hidden/block%', 'LIKE')
-      ->orderBy('ml.router_path')
-      ->execute()
-      ->fetchAllAssoc('router_path', PDO::FETCH_ASSOC);
+    $query = entity_query('menu_link')
+      ->condition('router_path', 'menu-test/hidden/block', 'STARTS_WITH')
+      ->sort('router_path');
+    $result = $query->execute();
+    $menu_links = menu_link_load_multiple($result);
+
+    $links = array();
+    foreach ($menu_links as $menu_link) {
+      $links[$menu_link->router_path] = $menu_link;
+    }
 
     $parent = $links['menu-test/hidden/block'];
     $depth = $parent['depth'] + 1;
@@ -464,7 +475,7 @@ function testMenuItemHooks() {
    */
   function testMenuLinkOptions() {
     // Create a menu link with options.
-    $menu_link = array(
+    $menu_link = entity_create('menu_link', array(
       'link_title' => 'Menu link options test',
       'link_path' => 'test-page',
       'module' => 'menu_test',
@@ -476,7 +487,7 @@ function testMenuLinkOptions() {
           'testparam' => 'testvalue',
         ),
       ),
-    );
+    ));
     menu_link_save($menu_link);
 
     // Load front page.
diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeDataUnitTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/TreeDataUnitTest.php
index fac7841..1a7de01 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeDataUnitTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Menu/TreeDataUnitTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system\Tests\Menu;
 
+use Drupal\menu_link\Plugin\Core\Entity\MenuLink;
 use Drupal\simpletest\UnitTestBase;
 
 /**
@@ -16,13 +17,7 @@ class TreeDataUnitTest extends UnitTestBase {
   /**
    * Dummy link structure acceptable for menu_tree_data().
    */
-  var $links = array(
-    1 => array('mlid' => 1, 'depth' => 1),
-    2 => array('mlid' => 2, 'depth' => 1),
-    3 => array('mlid' => 3, 'depth' => 2),
-    4 => array('mlid' => 4, 'depth' => 3),
-    5 => array('mlid' => 5, 'depth' => 1),
-  );
+  protected $links = array();
 
   public static function getInfo() {
     return array(
@@ -35,7 +30,15 @@ public static function getInfo() {
   /**
    * Validate the generation of a proper menu tree hierarchy.
    */
-  function testMenuTreeData() {
+  public function testMenuTreeData() {
+    $this->links = array(
+      1 => new MenuLink(array('mlid' => 1, 'depth' => 1), 'menu_link'),
+      2 => new MenuLink(array('mlid' => 2, 'depth' => 1), 'menu_link'),
+      3 => new MenuLink(array('mlid' => 3, 'depth' => 2), 'menu_link'),
+      4 => new MenuLink(array('mlid' => 4, 'depth' => 3), 'menu_link'),
+      5 => new MenuLink(array('mlid' => 5, 'depth' => 1), 'menu_link'),
+    );
+
     $tree = menu_tree_data($this->links);
 
     // Validate that parent items #1, #2, and #5 exist on the root level.
diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeOutputTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/TreeOutputTest.php
index e1b46ad..7e09d18 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeOutputTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Menu/TreeOutputTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system\Tests\Menu;
 
+use Drupal\menu_link\Plugin\Core\Entity\MenuLink;
 use Drupal\simpletest\WebTestBase;
 
 /**
@@ -16,24 +17,7 @@ class TreeOutputTest extends WebTestBase {
   /**
    * Dummy link structure acceptable for menu_tree_output().
    */
-  var $tree_data = array(
-    '1'=> array(
-      'link' => array( 'menu_name' => 'main-menu', 'mlid' => 1, 'hidden'=>0, 'has_children' => 1, 'title' => 'Item 1', 'in_active_trail' => 1, 'access'=>1, 'href' => 'a', 'localized_options' => array('attributes' => array('title' =>'')) ),
-      'below' => array(
-        '2' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 2, 'hidden'=>0, 'has_children' => 1, 'title' => 'Item 2', 'in_active_trail' => 1, 'access'=>1, 'href' => 'a/b', 'localized_options' => array('attributes' => array('title' =>'')) ),
-          'below' => array(
-            '3' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 3, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 3', 'in_active_trail' => 0, 'access'=>1, 'href' => 'a/b/c', 'localized_options' => array('attributes' => array('title' =>'')) ),
-              'below' => array() ),
-            '4' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 4, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 4', 'in_active_trail' => 0, 'access'=>1, 'href' => 'a/b/d', 'localized_options' => array('attributes' => array('title' =>'')) ),
-              'below' => array() )
-            )
-          )
-        )
-      ),
-    '5' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 5, 'hidden'=>1, 'has_children' => 0, 'title' => 'Item 5', 'in_active_trail' => 0, 'access'=>1, 'href' => 'e', 'localized_options' => array('attributes' => array('title' =>'')) ), 'below' => array( ) ),
-    '6' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 6, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 6', 'in_active_trail' => 0, 'access'=>0, 'href' => 'f', 'localized_options' => array('attributes' => array('title' =>'')) ), 'below' => array( ) ),
-    '7' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 7, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 7', 'in_active_trail' => 0, 'access'=>1, 'href' => 'g', 'localized_options' => array('attributes' => array('title' =>'')) ), 'below' => array( ) )
-  );
+  protected $tree_data = array();
 
   public static function getInfo() {
     return array(
@@ -51,6 +35,26 @@ function setUp() {
    * Validate the generation of a proper menu tree output.
    */
   function testMenuTreeData() {
+    // @todo Prettify this tree buildup code, it's very hard to read.
+    $this->tree_data = array(
+      '1'=> array(
+        'link' => new MenuLink(array('menu_name' => 'main-menu', 'mlid' => 1, 'hidden' => 0, 'has_children' => 1, 'title' => 'Item 1', 'in_active_trail' => 1, 'access' => 1, 'href' => 'a', 'localized_options' => array('attributes' => array('title' =>''))), 'menu_link'),
+        'below' => array(
+          '2' => array('link' => new MenuLink(array('menu_name' => 'main-menu', 'mlid' => 2, 'hidden' => 0, 'has_children' => 1, 'title' => 'Item 2', 'in_active_trail' => 1, 'access' => 1, 'href' => 'a/b', 'localized_options' => array('attributes' => array('title' =>''))), 'menu_link'),
+            'below' => array(
+              '3' => array('link' => new MenuLink(array('menu_name' => 'main-menu', 'mlid' => 3, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 3', 'in_active_trail' => 0, 'access' => 1, 'href' => 'a/b/c', 'localized_options' => array('attributes' => array('title' =>''))), 'menu_link'),
+                'below' => array() ),
+              '4' => array('link' => new MenuLink(array('menu_name' => 'main-menu', 'mlid' => 4, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 4', 'in_active_trail' => 0, 'access' => 1, 'href' => 'a/b/d', 'localized_options' => array('attributes' => array('title' =>''))), 'menu_link'),
+                'below' => array() )
+              )
+            )
+          )
+        ),
+      '5' => array('link' => new MenuLink(array('menu_name' => 'main-menu', 'mlid' => 5, 'hidden' => 1, 'has_children' => 0, 'title' => 'Item 5', 'in_active_trail' => 0, 'access' => 1, 'href' => 'e', 'localized_options' => array('attributes' => array('title' =>''))), 'menu_link'), 'below' => array()),
+      '6' => array('link' => new MenuLink(array('menu_name' => 'main-menu', 'mlid' => 6, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 6', 'in_active_trail' => 0, 'access' => 0, 'href' => 'f', 'localized_options' => array('attributes' => array('title' =>''))), 'menu_link'), 'below' => array()),
+      '7' => array('link' => new MenuLink(array('menu_name' => 'main-menu', 'mlid' => 7, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 7', 'in_active_trail' => 0, 'access' => 1, 'href' => 'g', 'localized_options' => array('attributes' => array('title' =>''))), 'menu_link'), 'below' => array())
+    );
+
     $output = menu_tree_output($this->tree_data);
 
     // Validate that the - in main-menu is changed into an underscore
diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc
index a5194f7..ff03b54 100644
--- a/core/modules/system/system.admin.inc
+++ b/core/modules/system/system.admin.inc
@@ -19,13 +19,19 @@ function system_admin_config_page() {
     drupal_set_message(t('One or more problems were detected with your Drupal installation. Check the <a href="@status">status report</a> for more information.', array('@status' => url('admin/reports/status'))), 'error');
   }
   $blocks = array();
-  if ($admin = db_query("SELECT menu_name, mlid FROM {menu_links} WHERE link_path = 'admin/config' AND module = 'system'")->fetchAssoc()) {
-    $result = db_query("
-      SELECT m.*, ml.*
-      FROM {menu_links} ml
-      INNER JOIN {menu_router} m ON ml.router_path = m.path
-      WHERE ml.link_path <> 'admin/help' AND menu_name = :menu_name AND ml.plid = :mlid AND hidden = 0", $admin, array('fetch' => PDO::FETCH_ASSOC));
-    foreach ($result as $item) {
+  if ($system_link = entity_load_multiple_by_properties('menu_link', array('link_path' => 'admin/config', 'module' => 'system'))) {
+    $system_link = reset($system_link);
+    $query = entity_query('menu_link')
+      ->condition('link_path', 'admin/help', '<>')
+      ->condition('menu_name', $system_link->menu_name)
+      ->condition('plid', $system_link->id())
+      ->condition('hidden', 0);
+    $result = $query->execute();
+    if (!empty($result)) {
+      $menu_links = menu_link_load_multiple($result);
+    }
+
+    foreach ($menu_links as $item) {
       _menu_link_translate($item);
       if (!$item['access']) {
         continue;
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 07a5638..56f5350 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -917,127 +917,6 @@ function hook_menu_alter(&$items) {
 }
 
 /**
- * Alter the data being saved to the {menu_links} table by menu_link_save().
- *
- * @param $item
- *   Associative array defining a menu link as passed into menu_link_save().
- *
- * @see hook_translated_menu_link_alter()
- */
-function hook_menu_link_alter(&$item) {
-  // Make all new admin links hidden (a.k.a disabled).
-  if (strpos($item['link_path'], 'admin') === 0 && empty($item['mlid'])) {
-    $item['hidden'] = 1;
-  }
-  // Flag a link to be altered by hook_translated_menu_link_alter().
-  if ($item['link_path'] == 'devel/cache/clear') {
-    $item['options']['alter'] = TRUE;
-  }
-  // Flag a link to be altered by hook_translated_menu_link_alter(), but only
-  // if it is derived from a menu router item; i.e., do not alter a custom
-  // menu link pointing to the same path that has been created by a user.
-  if ($item['link_path'] == 'user' && $item['module'] == 'system') {
-    $item['options']['alter'] = TRUE;
-  }
-}
-
-/**
- * Alter a menu link after it has been translated and before it is rendered.
- *
- * This hook is invoked from _menu_link_translate() after a menu link has been
- * translated; i.e., after dynamic path argument placeholders (%) have been
- * replaced with actual values, the user access to the link's target page has
- * been checked, and the link has been localized. It is only invoked if
- * $item['options']['alter'] has been set to a non-empty value (e.g., TRUE).
- * This flag should be set using hook_menu_link_alter().
- *
- * Implementations of this hook are able to alter any property of the menu link.
- * For example, this hook may be used to add a page-specific query string to all
- * menu links, or hide a certain link by setting:
- * @code
- *   'hidden' => 1,
- * @endcode
- *
- * @param $item
- *   Associative array defining a menu link after _menu_link_translate()
- * @param $map
- *   Associative array containing the menu $map (path parts and/or objects).
- *
- * @see hook_menu_link_alter()
- */
-function hook_translated_menu_link_alter(&$item, $map) {
-  if ($item['href'] == 'devel/cache/clear') {
-    $item['localized_options']['query'] = drupal_get_destination();
-  }
-}
-
-/**
- * Inform modules that a menu link has been created.
- *
- * This hook is used to notify modules that menu items have been
- * created. Contributed modules may use the information to perform
- * actions based on the information entered into the menu system.
- *
- * @param $link
- *   Associative array defining a menu link as passed into menu_link_save().
- *
- * @see hook_menu_link_update()
- * @see hook_menu_link_delete()
- */
-function hook_menu_link_insert($link) {
-  // In our sample case, we track menu items as editing sections
-  // of the site. These are stored in our table as 'disabled' items.
-  $record['mlid'] = $link['mlid'];
-  $record['menu_name'] = $link['menu_name'];
-  $record['status'] = 0;
-  drupal_write_record('menu_example', $record);
-}
-
-/**
- * Inform modules that a menu link has been updated.
- *
- * This hook is used to notify modules that menu items have been
- * updated. Contributed modules may use the information to perform
- * actions based on the information entered into the menu system.
- *
- * @param $link
- *   Associative array defining a menu link as passed into menu_link_save().
- *
- * @see hook_menu_link_insert()
- * @see hook_menu_link_delete()
- */
-function hook_menu_link_update($link) {
-  // If the parent menu has changed, update our record.
-  $menu_name = db_query("SELECT menu_name FROM {menu_example} WHERE mlid = :mlid", array(':mlid' => $link['mlid']))->fetchField();
-  if ($menu_name != $link['menu_name']) {
-    db_update('menu_example')
-      ->fields(array('menu_name' => $link['menu_name']))
-      ->condition('mlid', $link['mlid'])
-      ->execute();
-  }
-}
-
-/**
- * Inform modules that a menu link has been deleted.
- *
- * This hook is used to notify modules that menu items have been
- * deleted. Contributed modules may use the information to perform
- * actions based on the information entered into the menu system.
- *
- * @param $link
- *   Associative array defining a menu link as passed into menu_link_save().
- *
- * @see hook_menu_link_insert()
- * @see hook_menu_link_update()
- */
-function hook_menu_link_delete($link) {
-  // Delete the record from our table.
-  db_delete('menu_example')
-    ->condition('mlid', $link['mlid'])
-    ->execute();
-}
-
-/**
  * Alter tabs and actions displayed on the page before they are rendered.
  *
  * This hook is invoked by menu_local_tasks(). The system-determined tabs and
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 8c7d72a..a027d96 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -950,192 +950,6 @@ function system_schema() {
     'primary key' => array('path'),
   );
 
-  $schema['menu_links'] = array(
-    'description' => 'Contains the individual links within a menu.',
-    '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,
-      ),
-      'plid' => array(
-        'description' => 'The parent link ID (plid) is the mlid of the link above in the hierarchy, or zero if the link is at the top level in its menu.',
-        'type' => 'int',
-        'unsigned' => TRUE,
-        'not null' => TRUE,
-        'default' => 0,
-      ),
-      'link_path' => array(
-        'description' => 'The Drupal path or external path this link points to.',
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'router_path' => array(
-        'description' => 'For links corresponding to a Drupal path (external = 0), this connects the link to a {menu_router}.path for joins.',
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-      ),
-      'link_title' => array(
-      'description' => 'The text displayed for the link, which may be modified by a title callback stored in {menu_router}.',
-        'type' => 'varchar',
-        'length' => 255,
-        'not null' => TRUE,
-        'default' => '',
-        'translatable' => TRUE,
-      ),
-      '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',
-        'not null' => FALSE,
-        'translatable' => TRUE,
-      ),
-      'module' => array(
-        'description' => 'The name of the module that generated this link.',
-        'type' => 'varchar',
-        'length' => 255,
-        '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, -1 = a menu callback, 0 = a normal, visible link)',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'size' => 'small',
-      ),
-      'external' => array(
-        'description' => 'A flag to indicate if the link points to a full URL starting with a protocol, like http:// (1 = external, 0 = internal).',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'size' => 'small',
-      ),
-      'has_children' => array(
-        'description' => 'Flag indicating whether any links have this link as a parent (1 = children exist, 0 = no children).',
-        '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,
-      ),
-      'depth' => array(
-        'description' => 'The depth relative to the top level. A link with plid == 0 will have depth == 1.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'size' => 'small',
-      ),
-      'customized' => array(
-        'description' => 'A flag to indicate that the user has manually created or edited the link (1 = customized, 0 = not customized).',
-        '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 plid. 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,
-      ),
-      'updated' => array(
-        'description' => 'Flag that indicates that this link was generated during the update from Drupal 5.',
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-        'size' => 'small',
-      ),
-    ),
-    'indexes' => array(
-      'path_menu' => array(array('link_path', 128), 'menu_name'),
-      'menu_plid_expand_child' => array('menu_name', 'plid', 'expanded', 'has_children'),
-      'menu_parents' => array('menu_name', 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9'),
-      'router_path' => array(array('router_path', 128)),
-    ),
-    'primary key' => array('mlid'),
-  );
-
   $schema['queue'] = array(
     'description' => 'Stores items in queues.',
     'fields' => array(
@@ -2326,6 +2140,42 @@ function system_update_8045() {
 }
 
 /**
+ * Enable the new Menu link module.
+ *
+ * Creates the langcode and UUID columns for menu links.
+ */
+function system_update_8046() {
+  // Enable the module without re-installing the schema.
+  update_module_enable(array('menu_link'));
+
+  // Add the langcode column if it doesn't exist.
+  if (!db_field_exists('menu_links', 'langcode')) {
+    $column = array(
+      'description' => 'The {language}.langcode of this entity.',
+      'type' => 'varchar',
+      'length' => 12,
+      'not null' => TRUE,
+      'default' => '',
+    );
+    db_add_field('menu_links', 'langcode', $column);
+  }
+
+  // Add the UUID column.
+  $column = array(
+  'description' => 'Unique Key: Universally unique identifier for this entity.',
+  'type' => 'varchar',
+  'length' => 128,
+  'not null' => FALSE,
+  );
+  $keys = array(
+  'unique keys' => array(
+    'uuid' => array('uuid'),
+  ),
+  );
+  db_add_field('menu_links', 'uuid', $column, $keys);
+}
+
+/**
  * @} End of "defgroup updates-7.x-to-8.x".
  * The next series of updates should start at 9000.
  */
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 6dd33b1..62b2d84 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -2595,7 +2595,10 @@ function system_admin_menu_block($item) {
   }
 
   if (!isset($item['mlid'])) {
-    $item += db_query("SELECT mlid, menu_name FROM {menu_links} ml WHERE ml.router_path = :path AND module = 'system'", array(':path' => $item['path']))->fetchAssoc();
+    $menu_links = entity_load_multiple_by_properties('menu_link', array('router_path' => $item['path'], 'module' => 'system'));
+    $menu_link = reset($menu_links);
+    $item['mlid'] = $menu_link->id();
+    $item['menu_name'] = $menu_link->menu_name;
   }
 
   if (isset($cache[$item['mlid']])) {
@@ -2603,17 +2606,8 @@ function system_admin_menu_block($item) {
   }
 
   $content = array();
-  $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC));
-  $query->join('menu_router', 'm', 'm.path = ml.router_path');
-  $query
-    ->fields('ml')
-    // Weight should be taken from {menu_links}, not {menu_router}.
-    ->fields('m', array_diff(drupal_schema_fields_sql('menu_router'), array('weight')))
-    ->condition('ml.plid', $item['mlid'])
-    ->condition('ml.menu_name', $item['menu_name'])
-    ->condition('ml.hidden', 0);
-
-  foreach ($query->execute() as $link) {
+  $menu_links = entity_load_multiple_by_properties('menu_link', array('plid' => $item['mlid'], 'menu_name' => $item['menu_name'], 'hidden' => 0));
+  foreach ($menu_links as $link) {
     _menu_link_translate($link);
     if ($link['access']) {
       // The link description, either derived from 'description' in
@@ -3378,18 +3372,8 @@ function system_get_module_admin_tasks($module, $info) {
 
   if (!isset($links)) {
     $links = array();
-    $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC));
-    $query->join('menu_router', 'm', 'm.path = ml.router_path');
-    $query
-      ->fields('ml')
-      // Weight should be taken from {menu_links}, not {menu_router}.
-      ->fields('m', array_diff(drupal_schema_fields_sql('menu_router'), array('weight')))
-      ->condition('ml.link_path', 'admin/%', 'LIKE')
-      ->condition('ml.hidden', 0, '>=')
-      ->condition('ml.module', 'system')
-      ->condition('m.number_parts', 1, '>')
-      ->condition('m.page_callback', 'system_admin_menu_block_page', '<>');
-    foreach ($query->execute() as $link) {
+    $menu_links = entity_get_controller('menu_link')->loadModuleAdminTasks();
+    foreach ($menu_links as $link) {
       _menu_link_translate($link);
       if ($link['access']) {
         $links[$link['router_path']] = $link;
@@ -3441,6 +3425,7 @@ function system_get_module_admin_tasks($module, $info) {
       $item['title'] = t('Configure @module permissions', array('@module' => $info['name']));
       unset($item['description']);
       $item['localized_options']['fragment'] = 'module-' . $module;
+      $item = entity_create('menu_link', $item);
       $admin_tasks["admin/people/permissions#module-$module"] = $item;
     }
   }
diff --git a/core/modules/toolbar/toolbar.info b/core/modules/toolbar/toolbar.info
index a6fd841..16046ea 100644
--- a/core/modules/toolbar/toolbar.info
+++ b/core/modules/toolbar/toolbar.info
@@ -5,5 +5,6 @@ package = Core
 version = VERSION
 
 dependencies[] = breakpoint
+dependencies[] = menu_link
 
 configure = admin/structure/toolbar
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index e543326..5802eb8 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -358,8 +358,13 @@ function($object) {return $object->mediaQuery;},
  */
 function toolbar_get_menu_tree() {
   $tree = array();
-  $admin_link = db_query('SELECT * FROM {menu_links} WHERE menu_name = :menu_name AND module = :module AND link_path = :path', array(':menu_name' => 'admin', ':module' => 'system', ':path' => 'admin'))->fetchAssoc();
-  if ($admin_link) {
+  $query = entity_query('menu_link')
+    ->condition('menu_name', 'admin')
+    ->condition('module', 'system')
+    ->condition('link_path', 'admin');
+  $result = $query->execute();
+  if (!empty($result)) {
+    $admin_link = menu_link_load(reset($result));
     $tree = menu_build_tree('admin', array(
       'expanded' => array($admin_link['mlid']),
       'min_depth' => $admin_link['depth'] + 1,
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index aa5d085..4417b99 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -1294,13 +1294,13 @@ function user_menu_site_status_alter(&$menu_site_status, $path) {
 }
 
 /**
- * Implements hook_menu_link_alter().
+ * Implements hook_menu_link_presave().
  */
-function user_menu_link_alter(&$link) {
+function user_menu_link_presave($link) {
   // The path 'user' must be accessible for anonymous users, but only visible
   // for authenticated users. Authenticated users should see "My account", but
   // anonymous users should not see it at all. Therefore, invoke
-  // user_translated_menu_link_alter() to conditionally hide the link.
+  // user_menu_link_load() to conditionally hide the link.
   if ($link['link_path'] == 'user' && $link['module'] == 'system') {
     $link['options']['alter'] = TRUE;
   }
@@ -1324,12 +1324,14 @@ function user_menu_breadcrumb_alter(&$active_trail, $item) {
 }
 
 /**
- * Implements hook_translated_menu_link_alter().
+ * Implements hook_menu_link_load().
  */
-function user_translated_menu_link_alter(&$link) {
+function user_menu_link_load($menu_links) {
   // Hide the "User account" link for anonymous users.
-  if ($link['link_path'] == 'user' && $link['module'] == 'system' && !$GLOBALS['user']->uid) {
-    $link['hidden'] = 1;
+  foreach ($menu_links as $link) {
+    if ($link['link_path'] == 'user' && $link['module'] == 'system' && !$GLOBALS['user']->uid) {
+      $link['hidden'] = 1;
+    }
   }
 }
 
diff --git a/core/profiles/minimal/minimal.info b/core/profiles/minimal/minimal.info
index 545e85c..5276bb2 100644
--- a/core/profiles/minimal/minimal.info
+++ b/core/profiles/minimal/minimal.info
@@ -2,6 +2,7 @@ name = Minimal
 description = Build a custom site without pre-configured functionality. Suitable for advanced users.
 version = VERSION
 core = 8.x
+dependencies[] = menu_link
 dependencies[] = node
 dependencies[] = block
 dependencies[] = dblog
diff --git a/core/profiles/standard/standard.install b/core/profiles/standard/standard.install
index 0961820..11addaf 100644
--- a/core/profiles/standard/standard.install
+++ b/core/profiles/standard/standard.install
@@ -408,11 +408,11 @@ function standard_install() {
     ->execute();
 
   // Create a Home link in the main menu.
-  $item = array(
+  $item = entity_create('menu_link', array(
     'link_title' => st('Home'),
     'link_path' => '<front>',
     'menu_name' => 'main',
-  );
+  ));
   menu_link_save($item);
 
   // Enable the Contact link in the footer menu.
@@ -422,16 +422,16 @@ function standard_install() {
 
   // Populate the default shortcut set.
   $shortcut_set = shortcut_set_load(SHORTCUT_DEFAULT_SET_NAME);
-  $shortcut_set->links[] = array(
+  $shortcut_set->links[] = entity_create('menu_link', array(
     'link_path' => 'node/add',
     'link_title' => st('Add content'),
     'weight' => -20,
-  );
-  $shortcut_set->links[] = array(
+  ));
+  $shortcut_set->links[] = entity_create('menu_link', array(
     'link_path' => 'admin/content',
     'link_title' => st('Find content'),
     'weight' => -19,
-  );
+  ));
   shortcut_set_save($shortcut_set);
 
   // Enable the admin theme.
diff --git a/core/profiles/testing/testing.info b/core/profiles/testing/testing.info
index fff3df2..fcbe479 100644
--- a/core/profiles/testing/testing.info
+++ b/core/profiles/testing/testing.info
@@ -3,5 +3,6 @@ description = Minimal profile for running tests. Includes absolutely required mo
 version = VERSION
 core = 8.x
 hidden = TRUE
+dependencies[] = menu_link
 ; @todo Remove dependency on Node module.
 dependencies[] = node
