diff --git a/core/modules/book/book.admin.inc b/core/modules/book/book.admin.inc
index 611537b..04915f7 100644
--- a/core/modules/book/book.admin.inc
+++ b/core/modules/book/book.admin.inc
@@ -5,190 +5,6 @@
  * Administration page callbacks for the Book module.
  */
 
-use Drupal\Component\Utility\Crypt;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Language\Language;
-
-/**
- * Form constructor for administering a single book's hierarchy.
- *
- * @param \Drupal\Core\Entity\EntityInterface $node
- *   The node of the top-level page in the book.
- *
- * @see book_menu()
- * @see book_admin_edit_validate()
- * @see book_admin_edit_submit()
- * @ingroup forms
- */
-function book_admin_edit($form, $form_state, EntityInterface $node) {
-  drupal_set_title($node->label());
-  $form['#node'] = $node;
-  _book_admin_table($node, $form);
-  $form['save'] = array(
-    '#type' => 'submit',
-    '#value' => t('Save book pages'),
-  );
-
-  return $form;
-}
-
-/**
- * Form validation handler for book_admin_edit().
- *
- * Checks that the book has not been changed while using the form.
- *
- * @see book_admin_edit_submit()
- */
-function book_admin_edit_validate($form, &$form_state) {
-  if ($form_state['values']['tree_hash'] != $form_state['values']['tree_current_hash']) {
-    form_set_error('', t('This book has been modified by another user, the changes could not be saved.'));
-  }
-}
-
-/**
- * Form submission handler for book_admin_edit().
- *
- * This function takes care to save parent menu items before their children.
- * Saving menu items in the incorrect order can break the menu tree.
- *
- * @see book_admin_edit_validate()
- * @see menu_overview_form_submit()
- */
-function book_admin_edit_submit($form, &$form_state) {
-  // Save elements in the same order as defined in post rather than the form.
-  // This ensures parents are updated before their children, preventing orphans.
-  $order = array_flip(array_keys($form_state['input']['table']));
-  $form['table'] = array_merge($order, $form['table']);
-
-  // Track updates.
-  $updated = FALSE;
-  foreach (element_children($form['table']) as $key) {
-    if ($form['table'][$key]['#item']) {
-      $row = $form['table'][$key];
-      $values = $form_state['values']['table'][$key];
-
-      // Update menu item if moved.
-      if ($row['plid']['#default_value'] != $values['plid'] || $row['weight']['#default_value'] != $values['weight']) {
-        $menu_link = entity_load('menu_link', $values['mlid']);
-        $menu_link->weight = $values['weight'];
-        $menu_link->plid = $values['plid'];
-        $menu_link->save();
-        $updated = TRUE;
-      }
-
-      // Update the title if changed.
-      if ($row['title']['#default_value'] != $values['title']) {
-        $node = node_load($values['nid']);
-        $langcode = Language::LANGCODE_NOT_SPECIFIED;
-        $node->title = $values['title'];
-        $node->book['link_title'] = $values['title'];
-        $node->setNewRevision();
-        $node->log = t('Title changed from %original to %current.', array('%original' => $node->title, '%current' => $values['title']));
-
-        $node->save();
-        watchdog('content', 'book: updated %title.', array('%title' => $node->label()), WATCHDOG_NOTICE, l(t('view'), 'node/' . $node->id()));
-      }
-    }
-  }
-  if ($updated) {
-    // Flush static and cache.
-    drupal_static_reset('book_menu_subtree_data');
-    $cid = 'links:' . $form['#node']->book['menu_name'] . ':subtree-cid:' . $form['#node']->book['mlid'];
-    cache('menu')->delete($cid);
-  }
-
-  drupal_set_message(t('Updated book %title.', array('%title' => $form['#node']->label())));
-}
-
-/**
- * Builds the table portion of the form for the book administration page.
- *
- * @param \Drupal\Core\Entity\EntityInterface $node
- *   The node of the top-level page in the book.
- * @param $form
- *   The form that is being modified, passed by reference.
- *
- * @see book_admin_edit()
- */
-function _book_admin_table(EntityInterface $node, &$form) {
-  $form['table'] = array(
-    '#theme' => 'book_admin_table',
-    '#tree' => TRUE,
-  );
-
-  $tree = book_menu_subtree_data($node->book);
-  $tree = array_shift($tree); // Do not include the book item itself.
-  if ($tree['below']) {
-    $hash = Crypt::hashBase64(serialize($tree['below']));
-    // Store the hash value as a hidden form element so that we can detect
-    // if another user changed the book hierarchy.
-    $form['tree_hash'] = array(
-      '#type' => 'hidden',
-      '#default_value' => $hash,
-    );
-    $form['tree_current_hash'] = array(
-      '#type' => 'value',
-      '#value' => $hash,
-    );
-    _book_admin_table_tree($tree['below'], $form['table']);
-  }
-
-}
-
-/**
- * Helps build the main table in the book administration page form.
- *
- * @param $tree
- *   A subtree of the book menu hierarchy.
- * @param $form
- *   The form that is being modified, passed by reference.
- *
- * @return
- *   The modified form array.
- *
- * @see book_admin_edit()
- */
-function _book_admin_table_tree($tree, &$form) {
-  // The delta must be big enough to give each node a distinct value.
-  $count = count($tree);
-  $delta = ($count < 30) ? 15 : intval($count / 2) + 1;
-
-  foreach ($tree as $data) {
-    $form['book-admin-' . $data['link']['nid']] = array(
-      '#item' => $data['link'],
-      'nid' => array('#type' => 'value', '#value' => $data['link']['nid']),
-      'depth' => array('#type' => 'value', '#value' => $data['link']['depth']),
-      'href' => array('#type' => 'value', '#value' => $data['link']['href']),
-      'title' => array(
-        '#type' => 'textfield',
-        '#default_value' => $data['link']['link_title'],
-        '#maxlength' => 255,
-        '#size' => 40,
-      ),
-      'weight' => array(
-        '#type' => 'weight',
-        '#default_value' => $data['link']['weight'],
-        '#delta' => max($delta, abs($data['link']['weight'])),
-        '#title' => t('Weight for @title', array('@title' => $data['link']['title'])),
-        '#title_display' => 'invisible',
-      ),
-      'plid' => array(
-        '#type' => 'hidden',
-        '#default_value' => $data['link']['plid'],
-      ),
-      'mlid' => array(
-        '#type' => 'hidden',
-        '#default_value' => $data['link']['mlid'],
-      ),
-    );
-    if ($data['below']) {
-      _book_admin_table_tree($data['below'], $form);
-    }
-  }
-
-  return $form;
-}
-
 /**
  * Returns HTML for a book administration form.
  *
@@ -196,7 +12,7 @@ function _book_admin_table_tree($tree, &$form) {
  *   An associative array containing:
  *   - form: A render element representing the form.
  *
- * @see book_admin_table()
+ * @see \Drupal\book\Form\BookAdminEditForm::bookAdminTable()
  * @ingroup themeable
  */
 function theme_book_admin_table($variables) {
diff --git a/core/modules/book/book.module b/core/modules/book/book.module
index 22b9ea9..095fe53 100644
--- a/core/modules/book/book.module
+++ b/core/modules/book/book.module
@@ -79,6 +79,7 @@ function book_theme() {
     ),
     'book_admin_table' => array(
       'render element' => 'form',
+      'file' => 'book.admin.inc',
     ),
     'book_all_books_block' => array(
       'render element' => 'book_menus',
@@ -173,15 +174,6 @@ function book_menu() {
     'type' => MENU_LOCAL_TASK,
     'weight' => 100,
   );
-  $items['admin/structure/book/%node'] = array(
-    'title' => 'Re-order book pages and change titles',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('book_admin_edit', 3),
-    'access callback' => '_book_outline_access',
-    'access arguments' => array(3),
-    'type' => MENU_CALLBACK,
-    'file' => 'book.admin.inc',
-  );
   $items['book'] = array(
     'title' => 'Books',
     'route_name' => 'book_render',
@@ -429,7 +421,8 @@ function _book_add_form_elements(&$form, &$form_state, EntityInterface $node) {
 
   $form['book']['plid'] = _book_parent_select($node->book);
 
-  // @see _book_admin_table_tree(). The weight may be larger than 15.
+  // @see \Drupal\book\Form\BookAdminEditForm::bookAdminTableTree(). The weight
+  // may be larger than 15.
   $form['book']['weight'] = array(
     '#type' => 'weight',
     '#title' => t('Weight'),
diff --git a/core/modules/book/book.routing.yml b/core/modules/book/book.routing.yml
index b44ccfb..2aa7eea 100644
--- a/core/modules/book/book.routing.yml
+++ b/core/modules/book/book.routing.yml
@@ -28,3 +28,12 @@ book_export:
   requirements:
     _permission: 'access printer-friendly version'
     _entity_access: 'node.view'
+
+book_admin_edit:
+  pattern: '/admin/structure/book/{node}'
+  defaults:
+    _form: 'Drupal\book\Form\BookAdminEditForm'
+    _title: 'Re-order book pages and change titles'
+  requirements:
+    _book_outline_access: 'TRUE'
+    node: \d+
diff --git a/core/modules/book/book.services.yml b/core/modules/book/book.services.yml
index e54cc62..b6980ea 100644
--- a/core/modules/book/book.services.yml
+++ b/core/modules/book/book.services.yml
@@ -2,6 +2,12 @@ services:
   book.manager:
     class: Drupal\book\BookManager
     arguments: ['@database', '@entity.manager']
+
   book.export:
     class: Drupal\book\BookExport
     arguments: ['@entity.manager']
+
+  access_check.book.outline:
+    class: Drupal\book\Access\OutlineAccessCheck
+    tags:
+      - { name: access_check }
diff --git a/core/modules/book/lib/Drupal/book/Access/OutlineAccessCheck.php b/core/modules/book/lib/Drupal/book/Access/OutlineAccessCheck.php
new file mode 100644
index 0000000..44e8fd6
--- /dev/null
+++ b/core/modules/book/lib/Drupal/book/Access/OutlineAccessCheck.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\book\Access\OutlineAccessCheck.
+ */
+
+namespace Drupal\book\Access;
+
+use Drupal\Core\Access\AccessCheckInterface;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Access check for book admin ouline routes.
+ */
+class OutlineAccessCheck implements AccessCheckInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applies(Route $route) {
+    return array_key_exists('_book_outline_access', $route->getRequirements());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function access(Route $route, Request $request) {
+    return user_access('administer book outlines') && node_access('view', $request->attributes->get('node'));
+  }
+}
diff --git a/core/modules/book/lib/Drupal/book/Form/BookAdminEditForm.php b/core/modules/book/lib/Drupal/book/Form/BookAdminEditForm.php
new file mode 100644
index 0000000..86ede9f
--- /dev/null
+++ b/core/modules/book/lib/Drupal/book/Form/BookAdminEditForm.php
@@ -0,0 +1,245 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\book\Form\BookAdminEditForm.
+ */
+
+namespace Drupal\book\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Component\Utility\Crypt;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
+use Drupal\menu_link\MenuLinkStorageControllerInterface;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Language\Language;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a form for administering a single book's hierarchy.
+ */
+class BookAdminEditForm extends FormBase implements ContainerInjectionInterface {
+
+  /**
+   * The menu cache object for this controller.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $cache;
+
+  /**
+   * The node storage controller.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageControllerInterface
+   */
+  protected $nodeStorage;
+
+  /**
+   * The menu link storage controller.
+   *
+   * @var \Drupal\menu_link\MenuLinkStorageControllerInterface
+   */
+  protected $menuLinkStorage;
+
+  /**
+   * Constructs a new BookAdminEditForm.
+   *
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
+   *   The menu cache object to be used by this controller.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageControllerInterface $node_storage
+   *   The custom block storage controller.
+   *
+   * @param \Drupal\Core\Entity\MenuLinkStorageControllerInterface $menu_link_storage
+   *   The custom block type storage controller.
+   */
+  public function __construct(CacheBackendInterface $cache, EntityStorageControllerInterface $node_storage, MenuLinkStorageControllerInterface $menu_link_storage) {
+    $this->cache = $cache;
+    $this->nodeStorage = $node_storage;
+    $this->menuLinkStorage = $menu_link_storage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    $entity_manager = $container->get('entity.manager');
+    return new static(
+      $container->get('cache.menu'),
+      $entity_manager->getStorageController('node'),
+      $entity_manager->getStorageController('menu_link')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormID() {
+    return 'book_admin_edit';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state, EntityInterface $node = NULL) {
+    $form['#title'] = $node->label();
+    $form['#node'] = $node;
+    $this->bookAdminTable($node, $form);
+    $form['save'] = array(
+      '#type' => 'submit',
+      '#value' => $this->t('Save book pages'),
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, array &$form_state) {
+    if ($form_state['values']['tree_hash'] != $form_state['values']['tree_current_hash']) {
+      form_set_error('', $this->t('This book has been modified by another user, the changes could not be saved.'));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, array &$form_state) {
+    // Save elements in the same order as defined in post rather than the form.
+    // This ensures parents are updated before their children, preventing orphans.
+    $order = array_flip(array_keys($form_state['input']['table']));
+    $form['table'] = array_merge($order, $form['table']);
+
+    // Track updates.
+    $updated = FALSE;
+    foreach (element_children($form['table']) as $key) {
+      if ($form['table'][$key]['#item']) {
+        $row = $form['table'][$key];
+        $values = $form_state['values']['table'][$key];
+
+        // Update menu item if moved.
+        if ($row['plid']['#default_value'] != $values['plid'] || $row['weight']['#default_value'] != $values['weight']) {
+          $menu_link = $this->menuLinkStorage->load($values['mlid']);
+          $menu_link->weight = $values['weight'];
+          $menu_link->plid = $values['plid'];
+          $menu_link->save();
+          $updated = TRUE;
+        }
+
+        // Update the title if changed.
+        if ($row['title']['#default_value'] != $values['title']) {
+          $node = $this->nodeStorage->load($values['nid']);
+          $langcode = Language::LANGCODE_NOT_SPECIFIED;
+          $node->title = $values['title'];
+          $node->book['link_title'] = $values['title'];
+          $node->setNewRevision();
+          $node->log = $this->t('Title changed from %original to %current.', array('%original' => $node->title, '%current' => $values['title']));
+
+          $node->save();
+          watchdog('content', 'book: updated %title.', array('%title' => $node->label()), WATCHDOG_NOTICE, l($this->t('view'), 'node/' . $node->nid));
+        }
+      }
+    }
+    if ($updated) {
+      // Flush static and cache.
+      drupal_static_reset('book_menu_subtree_data');
+      $cid = 'links:' . $form['#node']->book['menu_name'] . ':subtree-cid:' . $form['#node']->book['mlid'];
+      $this->cache->delete($cid);
+    }
+
+    drupal_set_message($this->t('Updated book %title.', array('%title' => $form['#node']->label())));
+  }
+
+  /**
+   * Builds the table portion of the form for the book administration page.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $node
+   *   The node of the top-level page in the book.
+   * @param $form
+   *   The form that is being modified, passed by reference.
+   *
+   * @see \Drupal\book\Form\BookAdminEditForm::buildForm()
+   */
+  protected function bookAdminTable(EntityInterface $node, &$form) {
+    $form['table'] = array(
+      '#theme' => 'book_admin_table',
+      '#tree' => TRUE,
+    );
+
+    $tree = book_menu_subtree_data($node->book);
+    // Do not include the book item itself.
+    $tree = array_shift($tree);
+    if ($tree['below']) {
+      $hash = Crypt::hashBase64(serialize($tree['below']));
+      // Store the hash value as a hidden form element so that we can detect
+      // if another user changed the book hierarchy.
+      $form['tree_hash'] = array(
+        '#type' => 'hidden',
+        '#default_value' => $hash,
+      );
+      $form['tree_current_hash'] = array(
+        '#type' => 'value',
+        '#value' => $hash,
+      );
+      $this->bookAdminTableTree($tree['below'], $form['table']);
+    }
+  }
+
+  /**
+   * Helps build the main table in the book administration page form.
+   *
+   * @param $tree
+   *   A subtree of the book menu hierarchy.
+   * @param $form
+   *   The form that is being modified, passed by reference.
+   *
+   * @return
+   *   The modified form array.
+   *
+   * @see \Drupal\book\Form\BookAdminEditForm::buildForm()
+   */
+  protected function bookAdminTableTree($tree, &$form) {
+    // The delta must be big enough to give each node a distinct value.
+    $count = count($tree);
+    $delta = ($count < 30) ? 15 : intval($count / 2) + 1;
+
+    foreach ($tree as $data) {
+      $form['book-admin-' . $data['link']['nid']] = array(
+        '#item' => $data['link'],
+        'nid' => array('#type' => 'value', '#value' => $data['link']['nid']),
+        'depth' => array('#type' => 'value', '#value' => $data['link']['depth']),
+        'href' => array('#type' => 'value', '#value' => $data['link']['href']),
+        'title' => array(
+          '#type' => 'textfield',
+          '#default_value' => $data['link']['link_title'],
+          '#maxlength' => 255,
+          '#size' => 40,
+        ),
+        'weight' => array(
+          '#type' => 'weight',
+          '#default_value' => $data['link']['weight'],
+          '#delta' => max($delta, abs($data['link']['weight'])),
+          '#title' => $this->t('Weight for @title', array('@title' => $data['link']['title'])),
+          '#title_display' => 'invisible',
+        ),
+        'plid' => array(
+          '#type' => 'hidden',
+          '#default_value' => $data['link']['plid'],
+        ),
+        'mlid' => array(
+          '#type' => 'hidden',
+          '#default_value' => $data['link']['mlid'],
+        ),
+      );
+      if ($data['below']) {
+        $this->bookAdminTableTree($data['below'], $form);
+      }
+    }
+
+    return $form;
+  }
+
+}
