diff --git a/core/modules/book/book.module b/core/modules/book/book.module index fca17ab8ab5b301467f3861553fa2eeaa559f0cb..efc718bdd09d3596902486a14ccbd59dcfceb556 100644 --- a/core/modules/book/book.module +++ b/core/modules/book/book.module @@ -147,21 +147,6 @@ function book_node_links_alter(array &$node_links, NodeInterface $node, array &$ } } -/** - * Returns an array of all books. - * - * @todo Remove in favor of BookManager Service. http://drupal.org/node/1963894 - * - * This list may be used for generating a list of all the books, or for building - * the options for a form select. - * - * @return - * An array of all books. - */ -function book_get_books() { - return \Drupal::service('book.manager')->getAllBooks(); -} - /** * Implements hook_form_BASE_FORM_ID_alter() for node_form(). * @@ -244,164 +229,14 @@ function book_form_update($form, $form_state) { return $form['book']['pid']; } -/** - * Gets the book menu tree for a page and returns it as a linear array. - * - * @param $book_link - * A fully loaded menu link that is part of the book hierarchy. - * - * @return - * A linear array of menu links in the order that the links are shown in the - * menu, so the previous and next pages are the elements before and after the - * element corresponding to the current node. The children of the current node - * (if any) will come immediately after it in the array, and links will only - * be fetched as deep as one level deeper than $book_link. - */ -function book_get_flat_menu($book_link) { - $flat = &drupal_static(__FUNCTION__, array()); - - if (!isset($flat[$book_link['nid']])) { - // Call bookTreeAllData() to take advantage of caching. - $tree = \Drupal::service('book.manager')->bookTreeAllData($book_link['bid'], $book_link, $book_link['depth'] + 1); - $flat[$book_link['nid']] = array(); - _book_flatten_menu($tree, $flat[$book_link['nid']]); - } - - return $flat[$book_link['nid']]; -} - -/** - * Recursively converts a tree of menu links to a flat array. - * - * @param $tree - * A tree of menu links in an array. - * @param $flat - * A flat array of the menu links from $tree, passed by reference. - * - * @see book_get_flat_menu(). - */ -function _book_flatten_menu($tree, &$flat) { - foreach ($tree as $data) { - $flat[$data['link']['nid']] = $data['link']; - if ($data['below']) { - _book_flatten_menu($data['below'], $flat); - } - } -} - -/** - * Fetches the menu link for the previous page of the book. - * - * @param $book_link - * A fully loaded menu link that is part of the book hierarchy. - * - * @return - * A fully loaded menu link for the page before the one represented in - * $book_link. - */ -function book_prev($book_link) { - // If the parent is zero, we are at the start of a book. - if ($book_link['pid'] == 0) { - return NULL; - } - // Assigning the array to $flat resets the array pointer for use with each(). - $flat = book_get_flat_menu($book_link); - $curr = NULL; - do { - $prev = $curr; - list($key, $curr) = each($flat); - } while ($key && $key != $book_link['nid']); - - if ($key == $book_link['nid']) { - /** @var \Drupal\book\BookManagerInterface $book_manager */ - $book_manager = \Drupal::service('book.manager'); - // The previous page in the book may be a child of the previous visible link. - if ($prev['depth'] == $book_link['depth']) { - // The subtree will have only one link at the top level - get its data. - $tree = $book_manager->bookMenuSubtreeData($prev); - $data = array_shift($tree); - // The link of interest is the last child - iterate to find the deepest one. - while ($data['below']) { - $data = end($data['below']); - } - $book_manager->bookLinkTranslate($data['link']); - return $data['link']; - } - else { - $book_manager->bookLinkTranslate($prev); - return $prev; - } - } -} - -/** - * Fetches the menu link for the next page of the book. - * - * @param $book_link - * A fully loaded menu link that is part of the book hierarchy. - * - * @return - * A fully loaded book link for the page after the one represented in - * $book_link. - */ -function book_next($book_link) { - // Assigning the array to $flat resets the array pointer for use with each(). - $flat = book_get_flat_menu($book_link); - do { - list($key, ) = each($flat); - } - while ($key && $key != $book_link['nid']); - - if ($key == $book_link['nid']) { - $next = current($flat); - if ($next) { - \Drupal::service('book.manager')->bookLinkTranslate($next); - } - return $next; - } -} - -/** - * Formats the menu links for the child pages of the current page. - * - * @param $book_link - * A fully loaded menu link that is part of the book hierarchy. - * - * @return - * HTML for the links to the child pages of the current page. - */ -function book_children($book_link) { - $flat = book_get_flat_menu($book_link); - - $children = array(); - - if ($book_link['has_children']) { - // Walk through the array until we find the current page. - do { - $link = array_shift($flat); - } - while ($link && ($link['nid'] != $book_link['nid'])); - // Continue though the array and collect the links whose parent is this page. - while (($link = array_shift($flat)) && $link['pid'] == $book_link['nid']) { - $data['link'] = $link; - $data['below'] = ''; - $children[] = $data; - } - } - - if ($children) { - $elements = \Drupal::service('book.manager')->bookTreeOutput($children); - return drupal_render($elements); - } - return ''; -} - /** * Implements hook_node_load(). */ function book_node_load($nodes) { - $result = db_query("SELECT * FROM {book} WHERE nid IN (:nids)", array(':nids' => array_keys($nodes)), array('fetch' => PDO::FETCH_ASSOC)); - foreach ($result as $record) { + /** @var \Drupal\book\BookManagerInterface $book_manager */ + $book_manager = \Drupal::service('book.manager'); + $links = $book_manager->loadBookLinks(array_keys($nodes), FALSE); + foreach ($links as $record) { $nodes[$record['nid']]->book = $record; $nodes[$record['nid']]->book['link_path'] = 'node/' . $record['nid']; $nodes[$record['nid']]->book['link_title'] = $nodes[$record['nid']]->label(); @@ -442,28 +277,18 @@ function book_node_presave(EntityInterface $node) { * Implements hook_node_insert(). */ function book_node_insert(EntityInterface $node) { + /** @var \Drupal\book\BookManagerInterface $book_manager */ $book_manager = \Drupal::service('book.manager'); - if (!empty($node->book['bid'])) { - if ($node->book['bid'] == 'new') { - // New nodes that are their own book. - $node->book['bid'] = $node->id(); - } - $book_manager->updateOutline($node); - } + $book_manager->updateOutline($node); } /** * Implements hook_node_update(). */ function book_node_update(EntityInterface $node) { + /** @var \Drupal\book\BookManagerInterface $book_manager */ $book_manager = \Drupal::service('book.manager'); - if (!empty($node->book['bid'])) { - if ($node->book['bid'] == 'new') { - // New nodes that are their own book. - $node->book['bid'] = $node->id(); - } - $book_manager->updateOutline($node); - } + $book_manager->updateOutline($node); } /** @@ -481,7 +306,7 @@ function book_node_predelete(EntityInterface $node) { * Implements hook_node_prepare_form(). */ function book_node_prepare_form(NodeInterface $node, $operation, array &$form_state) { - // Get BookManager service + /** @var \Drupal\book\BookManagerInterface $book_manager */ $book_manager = \Drupal::service('book.manager'); // Prepare defaults for the add/edit form. @@ -584,12 +409,15 @@ function template_preprocess_book_navigation(&$variables) { $variables['current_depth'] = $book_link['depth']; $variables['tree'] = ''; + /** @var \Drupal\book\BookOutline $book_outline */ + $book_outline = \Drupal::service('book.outline'); + if ($book_link['nid']) { - $variables['tree'] = book_children($book_link); + $variables['tree'] = $book_outline->childrenLinks($book_link); $build = array(); - if ($prev = book_prev($book_link)) { + if ($prev = $book_outline->prevLink($book_link)) { $prev_href = \Drupal::url('node.view', array('node' => $prev['nid'])); $build['#attached']['drupal_add_html_head_link'][][] = array( 'rel' => 'prev', @@ -611,7 +439,7 @@ function template_preprocess_book_navigation(&$variables) { $variables['parent_title'] = String::checkPlain($parent['title']); } - if ($next = book_next($book_link)) { + if ($next = $book_outline->nextLink($book_link)) { $next_href = \Drupal::url('node.view', array('node' => $next['nid'])); $build['#attached']['drupal_add_html_head_link'][][] = array( 'rel' => 'next', diff --git a/core/modules/book/book.services.yml b/core/modules/book/book.services.yml index daa995a814fea79dc381e823d0710752164053d7..8dc0ec47f9ff9d3ad3e8213977346b9e453567d5 100644 --- a/core/modules/book/book.services.yml +++ b/core/modules/book/book.services.yml @@ -7,9 +7,12 @@ services: book.manager: class: Drupal\book\BookManager arguments: ['@database', '@entity.manager', '@string_translation', '@config.factory'] + book.outline: + class: Drupal\book\BookOutline + arguments: ['@book.manager'] book.export: class: Drupal\book\BookExport - arguments: ['@entity.manager'] + arguments: ['@entity.manager', '@book.manager'] access_check.book.removable: class: Drupal\book\Access\BookNodeIsRemovableAccessCheck diff --git a/core/modules/book/lib/Drupal/book/BookExport.php b/core/modules/book/lib/Drupal/book/BookExport.php index b03de15e5efea6426573fdee4f4a3ece528d7e04..f28f855dd8e111b2bad48b4d58942e048bf1629a 100644 --- a/core/modules/book/lib/Drupal/book/BookExport.php +++ b/core/modules/book/lib/Drupal/book/BookExport.php @@ -31,15 +31,25 @@ class BookExport { */ protected $viewBuilder; + /** + * The book manager. + * + * @var \Drupal\book\BookManagerInterface + */ + protected $bookManager; + /** * Constructs a BookExport object. * * @param \Drupal\Core\Entity\EntityManagerInterface $entityManager * The entity manager. + * @param \Drupal\book\BookManagerInterface $book_manager + * The book manager. */ - public function __construct(EntityManagerInterface $entityManager) { + public function __construct(EntityManagerInterface $entityManager, BookManagerInterface $book_manager) { $this->nodeStorage = $entityManager->getStorage('node'); $this->viewBuilder = $entityManager->getViewBuilder('node'); + $this->bookManager = $book_manager; } /** @@ -67,7 +77,7 @@ public function bookExportHtml(NodeInterface $node) { throw new \Exception(); } - $tree = \Drupal::service('book.manager')->bookMenuSubtreeData($node->book); + $tree = $this->bookManager->bookSubtreeData($node->book); $contents = $this->exportTraverse($tree, array($this, 'bookNodeExport')); return array( '#theme' => 'book_export_html', diff --git a/core/modules/book/lib/Drupal/book/BookManager.php b/core/modules/book/lib/Drupal/book/BookManager.php index e17f9e72d3f120143940a29784d21227d2e33556..04a16550ca5264fa3db16de690efff7f55403cdc 100644 --- a/core/modules/book/lib/Drupal/book/BookManager.php +++ b/core/modules/book/lib/Drupal/book/BookManager.php @@ -61,6 +61,13 @@ class BookManager implements BookManagerInterface { */ protected $books; + /** + * Stores flattened book trees. + * + * @var array + */ + protected $bookTreeFlattened; + /** * Constructs a BookManager object. */ @@ -135,25 +142,32 @@ public function getParentDepthLimit(array $book_link) { } /** - * {@inheritdoc} + * Determine the relative depth of the children of a given book link. + * + * @param array + * The book link. + * + * @return int + * The difference between the max depth in the book tree and the depth of + * the passed book link. */ - protected function findChildrenRelativeDepth(array $entity) { - $query = db_select('book'); + protected function findChildrenRelativeDepth(array $book_link) { + $query = $this->connection->select('book'); $query->addField('book', 'depth'); - $query->condition('bid', $entity['bid']); + $query->condition('bid', $book_link['bid']); $query->orderBy('depth', 'DESC'); $query->range(0, 1); $i = 1; $p = 'p1'; - while ($i <= static::BOOK_MAX_DEPTH && $entity[$p]) { - $query->condition($p, $entity[$p]); + while ($i <= static::BOOK_MAX_DEPTH && $book_link[$p]) { + $query->condition($p, $book_link[$p]); $p = 'p' . ++$i; } $max_depth = $query->execute()->fetchField(); - return ($max_depth > $entity['depth']) ? $max_depth - $entity['depth'] : 0; + return ($max_depth > $book_link['depth']) ? $max_depth - $book_link['depth'] : 0; } /** @@ -253,6 +267,12 @@ public function updateOutline(NodeInterface $node) { if (empty($node->book['bid'])) { return FALSE; } + + if (!empty($node->book['bid']) && $node->book['bid'] == 'new') { + // New nodes that are their own book. + $node->book['bid'] = $node->id(); + } + // Ensure we create a new book link if either the node itself is new, or the // bid was selected the first time, so that the original_bid is still empty. $new = empty($node->book['nid']) || empty($node->book['original_bid']); @@ -320,7 +340,7 @@ protected function t($string, array $args = array(), array $options = array()) { * existing form element. * * @param array $book_link - * A fully loaded menu link that is part of the book hierarchy. + * A fully loaded book link that is part of the book hierarchy. * * @return array * A parent selection form element. @@ -366,24 +386,24 @@ protected function addParentSelectFormElements(array $book_link) { } /** - * Recursively processes and formats menu items for getTableOfContents(). + * Recursively processes and formats book links for getTableOfContents(). * * This helper function recursively modifies the table of contents array for - * each item in the menu tree, ignoring items in the exclude array or at a depth + * each item in the book tree, ignoring items in the exclude array or at a depth * greater than the limit. Truncates titles over thirty characters and appends * an indentation string incremented by depth. * * @param array $tree - * The data structure of the book's menu tree. Includes hidden links. + * The data structure of the book's outline tree. Includes hidden links. * @param string $indent - * A string appended to each menu item title. Increments by '--' per depth + * A string appended to each node title. Increments by '--' per depth * level. * @param array $toc * Reference to the table of contents array. This is modified in place, so the * function does not have a return value. * @param array $exclude - * Optional array of menu link ID values. Any link whose menu link ID is in - * this array will be excluded (along with its children). + * Optional array of Node ID values. Any link whose node ID is in this + * array will be excluded (along with its children). * @param int $depth_limit * Any link deeper than this value will be excluded (along with its children). */ @@ -482,7 +502,7 @@ public function bookTreeAllData($bid, $link = NULL, $max_depth = NULL) { } // Build the tree using the parameters; the resulting tree will be cached. - $tree[$cid] = $this->menu_build_tree($bid, $tree_parameters); + $tree[$cid] = $this->bookTreeBuild($bid, $tree_parameters); } return $tree[$cid]; @@ -495,7 +515,7 @@ public function bookTreeOutput(array $tree) { $build = array(); $items = array(); - // Pull out just the menu links we are going to render so that we + // Pull out just the book links we are going to render so that we // get an accurate count for the first/last classes. foreach ($tree as $data) { if ($data['link']['access']) { @@ -514,7 +534,7 @@ public function bookTreeOutput(array $tree) { } // Set a class for the <li>-tag. Since $data['below'] may contain local // tasks, only set 'expanded' class if the link also has children within - // the current menu. + // the current book. if ($data['link']['has_children'] && $data['below']) { $class[] = 'expanded'; } @@ -530,11 +550,11 @@ public function bookTreeOutput(array $tree) { $data['link']['localized_options']['attributes']['class'][] = 'active-trail'; } - // Allow menu-specific theme overrides. + // Allow book-specific theme overrides. $element['#theme'] = 'book_link__book_toc_' . $data['link']['bid']; $element['#attributes']['class'] = $class; $element['#title'] = $data['link']['title']; - $node = \Drupal::entityManager()->getStorage('node')->load($data['link']['nid']); + $node = $this->entityManager->getStorage('node')->load($data['link']['nid']); $element['#href'] = $node->url(); $element['#localized_options'] = !empty($data['link']['localized_options']) ? $data['link']['localized_options'] : array(); $element['#below'] = $data['below'] ? $this->bookTreeOutput($data['below']) : $data['below']; @@ -554,47 +574,46 @@ public function bookTreeOutput(array $tree) { } /** - * Builds a menu tree, translates links, and checks access. + * Builds a book tree, translates links, and checks access. * * @param int $bid * The Book ID to find links for. * @param array $parameters * (optional) An associative array of build parameters. Possible keys: - * - expanded: An array of parent link ids to return only menu links that are - * children of one of the plids in this list. If empty, the whole menu tree + * - expanded: An array of parent link ids to return only book links that are + * children of one of the plids in this list. If empty, the whole outline * is built, unless 'only_active_trail' is TRUE. - * - active_trail: An array of mlids, representing the coordinates of the - * currently active menu link. + * - active_trail: An array of nids, representing the coordinates of the + * currently active book link. * - only_active_trail: Whether to only return links that are in the active * trail. This option is ignored, if 'expanded' is non-empty. - * - min_depth: The minimum depth of menu links in the resulting tree. - * Defaults to 1, which is the default to build a whole tree for a menu - * (excluding menu container itself). - * - max_depth: The maximum depth of menu links in the resulting tree. + * - min_depth: The minimum depth of book links in the resulting tree. + * Defaults to 1, which is the default to build a whole tree for a book. + * - max_depth: The maximum depth of book links in the resulting tree. * - conditions: An associative array of custom database select query * condition key/value pairs; see _menu_build_tree() for the actual query. * * @return array - * A fully built menu tree. + * A fully built book tree. */ - protected function menu_build_tree($bid, array $parameters = array()) { - // Build the menu tree. - $data = $this->_menu_build_tree($bid, $parameters); + protected function bookTreeBuild($bid, array $parameters = array()) { + // Build the book tree. + $data = $this->doBookTreeBuild($bid, $parameters); // Check access for the current user to each item in the tree. $this->bookTreeCheckAccess($data['tree'], $data['node_links']); return $data['tree']; } /** - * Builds a menu tree. + * Builds a book tree. * * This function may be used build the data for a menu tree only, for example * to further massage the data manually before further processing happens. - * menu_tree_check_access() needs to be invoked afterwards. + * _menu_tree_check_access() needs to be invoked afterwards. * * @see menu_build_tree() */ - protected function _menu_build_tree($bid, array $parameters = array()) { + protected function doBookTreeBuild($bid, array $parameters = array()) { // Static cache of already built menu trees. $trees = &drupal_static(__METHOD__, array()); $language_interface = \Drupal::languageManager()->getCurrentLanguage(); @@ -609,7 +628,7 @@ protected function _menu_build_tree($bid, array $parameters = array()) { // If we do not have this tree in the static cache, check {cache_data}. if (!isset($trees[$tree_cid])) { $cache = \Drupal::cache('data')->get($tree_cid); - if ($cache && isset($cache->data)) { + if ($cache && $cache->data) { $trees[$tree_cid] = $cache->data; } } @@ -646,7 +665,7 @@ protected function _menu_build_tree($bid, array $parameters = array()) { $links[$link['nid']] = $link; } $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array()); - $data['tree'] = $this->menu_tree_data($links, $active_trail, $min_depth); + $data['tree'] = $this->buildBookOutlineData($links, $active_trail, $min_depth); $data['node_links'] = array(); $this->bookTreeCollectNodeLinks($data['tree'], $data['node_links']); @@ -665,26 +684,70 @@ public function bookTreeCollectNodeLinks(&$tree, &$node_links) { // All book links are nodes. // @todo clean this up. foreach ($tree as $key => $v) { - if ($v['link']['nid']) { - $nid = $v['link']['nid']; - $node_links[$nid][$tree[$key]['link']['nid']] = &$tree[$key]['link']; - $tree[$key]['link']['access'] = FALSE; - } + $nid = $v['link']['nid']; + $node_links[$nid][$tree[$key]['link']['nid']] = &$tree[$key]['link']; + $tree[$key]['link']['access'] = FALSE; if ($tree[$key]['below']) { $this->bookTreeCollectNodeLinks($tree[$key]['below'], $node_links); } } } + /** + * {@inheritdoc} + */ + public function bookTreeGetFlat(array $book_link) { + if (!isset($this->bookTreeFlattened[$book_link['nid']])) { + // Call $this->bookTreeAllData() to take advantage of caching. + $tree = $this->bookTreeAllData($book_link['bid'], $book_link, $book_link['depth'] + 1); + $this->bookTreeFlattened[$book_link['nid']] = array(); + $this->flatBookTree($tree, $this->bookTreeFlattened[$book_link['nid']]); + } + + return $this->bookTreeFlattened[$book_link['nid']]; + } + + /** + * Recursively converts a tree of menu links to a flat array. + * + * @param array $tree + * A tree of menu links in an array. + * @param array $flat + * A flat array of the menu links from $tree, passed by reference. + * + * @see static::bookTreeGetFlat(). + */ + protected function flatBookTree(array $tree, array &$flat) { + foreach ($tree as $data) { + $flat[$data['link']['nid']] = $data['link']; + if ($data['below']) { + $this->flatBookTree($data['below'], $flat); + } + } + } + /** * {@inheritdoc} */ public function loadBookLink($nid, $translate = TRUE) { - $link = $this->connection->query("SELECT * FROM {book} WHERE nid = :nid", array(':nid' => $nid))->fetchAssoc(); - if ($link && $translate) { - $this->bookLinkTranslate($link); + $links = $this->loadBookLinks(array($nid), $translate); + return isset($links[$nid]) ? $links[$nid] : FALSE; + } + + /** + * {@inheritdoc} + */ + public function loadBookLinks($nids, $translate = TRUE) { + $result = $this->connection->query("SELECT * FROM {book} WHERE nid IN (:nids)", array(':nids' => $nids), array('fetch' => \PDO::FETCH_ASSOC)); + $links = array(); + foreach ($result as $link) { + if ($translate) { + $this->bookLinkTranslate($link); + } + $links[$link['nid']] = $link; } - return $link; + + return $links; } /** @@ -887,8 +950,9 @@ protected function setParents(array &$link, array $parent) { */ public function bookTreeCheckAccess(&$tree, $node_links = array()) { if ($node_links) { + // @todo Extract that into its own method. $nids = array_keys($node_links); - $select = db_select('node_field_data', 'n'); + $select = $this->connection->select('node_field_data', 'n'); $select->addField('n', 'nid'); // @todo This should be actually filtering on the desired node status field // language and just fall back to the default language. @@ -903,20 +967,20 @@ public function bookTreeCheckAccess(&$tree, $node_links = array()) { } } } - $this->_menu_tree_check_access($tree); + $this->doBookTreeCheckAccess($tree); } /** * Sorts the menu tree and recursively checks access for each item. */ - protected function _menu_tree_check_access(&$tree) { + protected function doBookTreeCheckAccess(&$tree) { $new_tree = array(); foreach ($tree as $key => $v) { $item = &$tree[$key]['link']; $this->bookLinkTranslate($item); if ($item['access']) { if ($tree[$key]['below']) { - $this->_menu_tree_check_access($tree[$key]['below']); + $this->doBookTreeCheckAccess($tree[$key]['below']); } // The weights are made a uniform 5 digits by adding 50000 as an offset. // After calling $this->bookLinkTranslate(), $item['title'] has the @@ -955,44 +1019,41 @@ public function bookLinkTranslate(&$link) { } /** - * Sorts and returns the built data representing a menu tree. + * Sorts and returns the built data representing a book tree. * * @param array $links - * A flat array of menu links that are part of the menu. Each array element - * is an associative array of information about the menu link, containing the - * fields from the {menu_links} table, and optionally additional information - * from the {menu_router} table, if the menu item appears in both tables. - * This array must be ordered depth-first. See _menu_build_tree() for a sample - * query. + * A flat array of book links that are part of the book. Each array element + * is an associative array of information about the book link, containing the + * fields from the {book} table. This array must be ordered depth-first. * @param array $parents - * An array of the menu link ID values that are in the path from the current - * page to the root of the menu tree. + * An array of the node ID values that are in the path from the current + * page to the root of the book tree. * @param int $depth - * The minimum depth to include in the returned menu tree. + * The minimum depth to include in the returned book tree. * * @return array - * An array of menu links in the form of a tree. Each item in the tree is an + * An array of book links in the form of a tree. Each item in the tree is an * associative array containing: - * - link: The menu link item from $links, with additional element + * - link: The book link item from $links, with additional element * 'in_active_trail' (TRUE if the link ID was in $parents). * - below: An array containing the sub-tree of this item, where each element * is a tree item array with 'link' and 'below' elements. This array will be - * empty if the menu item has no items in its sub-tree having a depth + * empty if the book link has no items in its sub-tree having a depth * greater than or equal to $depth. */ - protected function menu_tree_data(array $links, array $parents = array(), $depth = 1) { + protected function buildBookOutlineData(array $links, array $parents = array(), $depth = 1) { // Reverse the array so we can use the more efficient array_pop() function. $links = array_reverse($links); - return $this->_menu_tree_data($links, $parents, $depth); + return $this->buildBookOutlineRecursive($links, $parents, $depth); } /** - * Builds the data representing a menu tree. + * Builds the data representing a book tree. * * The function is a bit complex because the rendering of a link depends on - * the next menu link. + * the next book link. */ - protected function _menu_tree_data(&$links, $parents, $depth) { + protected function buildBookOutlineRecursive(&$links, $parents, $depth) { $tree = array(); while ($item = array_pop($links)) { // We need to determine if we're on the path to root so we can later build @@ -1008,8 +1069,8 @@ protected function _menu_tree_data(&$links, $parents, $depth) { $next = end($links); // Check whether the next link is the first in a new sub-tree. if ($next && $next['depth'] > $depth) { - // Recursively call _menu_tree_data to build the sub-tree. - $tree[$item['nid']]['below'] = $this->_menu_tree_data($links, $parents, $next['depth']); + // Recursively call buildBookOutlineRecursive to build the sub-tree. + $tree[$item['nid']]['below'] = $this->buildBookOutlineRecursive($links, $parents, $next['depth']); // Fetch next link after filling the sub-tree. $next = end($links); } @@ -1024,19 +1085,19 @@ protected function _menu_tree_data(&$links, $parents, $depth) { /** * {@inheritdoc} */ - public function bookMenuSubtreeData($link) { + public function bookSubtreeData($link) { $tree = &drupal_static(__METHOD__, array()); // Generate a cache ID (cid) specific for this $link. $cid = 'book-links:subtree-cid:' . $link['nid']; if (!isset($tree[$cid])) { - $cache = \Drupal::cache('data')->get($cid); + $tree_cid_cache = \Drupal::cache('data')->get($cid); - if ($cache && isset($cache->data)) { + if ($tree_cid_cache && $tree_cid_cache->data) { // If the cache entry exists, it will just be the cid for the actual data. // This avoids duplication of large amounts of data. - $cache = \Drupal::cache('data')->get($cache->data); + $cache = \Drupal::cache('data')->get($tree_cid_cache->data); if ($cache && isset($cache->data)) { $data = $cache->data; @@ -1058,7 +1119,7 @@ public function bookMenuSubtreeData($link) { foreach ($query->execute() as $item) { $links[] = $item; } - $data['tree'] = $this->menu_tree_data($links, array(), $link['depth']); + $data['tree'] = $this->buildBookOutlineData($links, array(), $link['depth']); $data['node_links'] = array(); $this->bookTreeCollectNodeLinks($data['tree'], $data['node_links']); // Compute the real cid for book subtree data. @@ -1068,7 +1129,7 @@ public function bookMenuSubtreeData($link) { if (!\Drupal::cache('data')->get($tree_cid)) { \Drupal::cache('data')->set($tree_cid, $data, Cache::PERMANENT, array('bid' => $link['bid'])); } - // Cache the cid of the (shared) data using the menu and item-specific cid. + // Cache the cid of the (shared) data using the book and item-specific cid. \Drupal::cache('data')->set($cid, $tree_cid, Cache::PERMANENT, array('bid' => $link['bid'])); } // Check access for the current user to each item in the tree. diff --git a/core/modules/book/lib/Drupal/book/BookManagerInterface.php b/core/modules/book/lib/Drupal/book/BookManagerInterface.php index 18725a98c583eed1f0e1eba7c27483663fe16fe7..c096e68df3f7e2621d29841edb08836c82e72e33 100644 --- a/core/modules/book/lib/Drupal/book/BookManagerInterface.php +++ b/core/modules/book/lib/Drupal/book/BookManagerInterface.php @@ -53,6 +53,20 @@ public function bookTreeAllData($bid, $link = NULL, $max_depth = NULL); */ public function loadBookLink($nid, $translate = TRUE); + /** + * Loads multiple book entries. + * + * @param int[] $nids + * An array of nids to load. + * + * @param bool $translate + * If TRUE, set access, title, and other elements. + * + * @return array[] + * The book data of each node keyed by NID. + */ + public function loadBookLinks($nids, $translate = TRUE); + /** * Returns an array of book pages in table of contents order. * @@ -94,10 +108,7 @@ public function getParentDepthLimit(array $book_link); public function bookTreeCollectNodeLinks(&$tree, &$node_links); /** - * Provides menu link access control, translation, and argument handling. - * - * This function is similar to _menu_translate(), but it also does - * link-specific preparation (such as always calling to_arg() functions). + * Provides book loading, access control and translation. * * @param array $link * A book link. @@ -107,6 +118,21 @@ public function bookTreeCollectNodeLinks(&$tree, &$node_links); */ public function bookLinkTranslate(&$link); + /** + * Gets the book for a page and returns it as a linear array. + * + * @param array $book_link + * A fully loaded book link that is part of the book hierarchy. + * + * @return array + * A linear array of book links in the order that the links are shown in the + * book, so the previous and next pages are the elements before and after the + * element corresponding to the current node. The children of the current node + * (if any) will come immediately after it in the array, and links will only + * be fetched as deep as one level deeper than $book_link. + */ + public function bookTreeGetFlat(array $book_link); + /** * Returns an array of all books. * @@ -195,7 +221,7 @@ public function deleteFromBook($nid); * - leaf: The menu item has no submenu. * * @param array $tree - * A data structure representing the tree as returned from menu_tree_data. + * A data structure representing the tree as returned from buildBookOutlineData. * * @return array * A structured array to be rendered by drupal_render(). @@ -224,12 +250,12 @@ public function bookTreeCheckAccess(&$tree, $node_links = array()); * tree. * * @param array $link - * A fully loaded menu link. + * A fully loaded book link. * * @return - * A subtree of menu links in an array, in the order they should be rendered. + * A subtree of book links in an array, in the order they should be rendered. */ - public function bookMenuSubtreeData($link); + public function bookSubtreeData($link); /** * Determines if a node can be removed from the book. diff --git a/core/modules/book/lib/Drupal/book/BookOutline.php b/core/modules/book/lib/Drupal/book/BookOutline.php new file mode 100644 index 0000000000000000000000000000000000000000..484fd2c29be8483f65573b74d1bf47c88ee86e1f --- /dev/null +++ b/core/modules/book/lib/Drupal/book/BookOutline.php @@ -0,0 +1,135 @@ +<?php + +/** + * @file + * Contains \Drupal\book\BookOutline. + */ + +namespace Drupal\book; + +/** + * Provides handling to render the book outline. + */ +class BookOutline { + + /** + * The book manager. + * + * @var \Drupal\book\BookManagerInterface + */ + protected $bookManager; + + /** + * Constructs a new BookOutline. + * + * @param \Drupal\book\BookManagerInterface $book_manager + * The book manager. + */ + public function __construct(BookManagerInterface $book_manager) { + $this->bookManager = $book_manager; + } + + /** + * Fetches the book link for the previous page of the book. + * + * @param array $book_link + * A fully loaded book link that is part of the book hierarchy. + * + * @return array + * A fully loaded book link for the page before the one represented in + * $book_link. + */ + public function prevLink(array $book_link) { + // If the parent is zero, we are at the start of a book. + if ($book_link['pid'] == 0) { + return NULL; + } + // Assigning the array to $flat resets the array pointer for use with each(). + $flat = $this->bookManager->bookTreeGetFlat($book_link); + $curr = NULL; + do { + $prev = $curr; + list($key, $curr) = each($flat); + } while ($key && $key != $book_link['nid']); + + if ($key == $book_link['nid']) { + // The previous page in the book may be a child of the previous visible link. + if ($prev['depth'] == $book_link['depth']) { + // The subtree will have only one link at the top level - get its data. + $tree = $this->bookManager->bookSubtreeData($prev); + $data = array_shift($tree); + // The link of interest is the last child - iterate to find the deepest one. + while ($data['below']) { + $data = end($data['below']); + } + $this->bookManager->bookLinkTranslate($data['link']); + return $data['link']; + } + else { + $this->bookManager->bookLinkTranslate($prev); + return $prev; + } + } + } + + /** + * Fetches the book link for the next page of the book. + * + * @param array $book_link + * A fully loaded book link that is part of the book hierarchy. + * + * @return array + * A fully loaded book link for the page after the one represented in + * $book_link. + */ + public function nextLink(array $book_link) { + // Assigning the array to $flat resets the array pointer for use with each(). + $flat = $this->bookManager->bookTreeGetFlat($book_link); + do { + list($key,) = each($flat); + } while ($key && $key != $book_link['nid']); + + if ($key == $book_link['nid']) { + $next = current($flat); + if ($next) { + $this->bookManager->bookLinkTranslate($next); + } + return $next; + } + } + + /** + * Formats the book links for the child pages of the current page. + * + * @param array $book_link + * A fully loaded book link that is part of the book hierarchy. + * + * @return array + * HTML for the links to the child pages of the current page. + */ + public function childrenLinks(array $book_link) { + $flat = $this->bookManager->bookTreeGetFlat($book_link); + + $children = array(); + + if ($book_link['has_children']) { + // Walk through the array until we find the current page. + do { + $link = array_shift($flat); + } while ($link && ($link['nid'] != $book_link['nid'])); + // Continue though the array and collect the links whose parent is this page. + while (($link = array_shift($flat)) && $link['pid'] == $book_link['nid']) { + $data['link'] = $link; + $data['below'] = ''; + $children[] = $data; + } + } + + if ($children) { + $elements = $this->bookManager->bookTreeOutput($children); + return drupal_render($elements); + } + return ''; + } + +} diff --git a/core/modules/book/lib/Drupal/book/Form/BookAdminEditForm.php b/core/modules/book/lib/Drupal/book/Form/BookAdminEditForm.php index e44a2c12a14be4bc22b95b6c32276513297e1357..ef9ed99dcdaed418614bac5b958d7348aae9a093 100644 --- a/core/modules/book/lib/Drupal/book/Form/BookAdminEditForm.php +++ b/core/modules/book/lib/Drupal/book/Form/BookAdminEditForm.php @@ -142,7 +142,7 @@ protected function bookAdminTable(NodeInterface $node, array &$form) { '#tree' => TRUE, ); - $tree = \Drupal::service('book.manager')->bookMenuSubtreeData($node->book); + $tree = $this->bookManager->bookSubtreeData($node->book); // Do not include the book item itself. $tree = array_shift($tree); if ($tree['below']) { diff --git a/core/modules/book/lib/Drupal/book/Plugin/Block/BookNavigationBlock.php b/core/modules/book/lib/Drupal/book/Plugin/Block/BookNavigationBlock.php index 6a9cfffd014d98c930e27d888d0b4c0e185b9db5..d98be3bccd89c2dfc2d6d4b84ed0c2e4e7fc9cd8 100644 --- a/core/modules/book/lib/Drupal/book/Plugin/Block/BookNavigationBlock.php +++ b/core/modules/book/lib/Drupal/book/Plugin/Block/BookNavigationBlock.php @@ -8,6 +8,7 @@ namespace Drupal\book\Plugin\Block; use Drupal\block\BlockBase; +use Drupal\book\BookManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; @@ -30,6 +31,13 @@ class BookNavigationBlock extends BlockBase implements ContainerFactoryPluginInt */ protected $request; + /** + * The book manager. + * + * @var \Drupal\book\BookManagerInterface + */ + protected $bookManager; + /** * Constructs a new BookNavigationBlock instance. * @@ -41,11 +49,14 @@ class BookNavigationBlock extends BlockBase implements ContainerFactoryPluginInt * The plugin implementation definition. * @param \Symfony\Component\HttpFoundation\Request $request * The request object. + * @param \Drupal\book\BookManagerInterface $book_manager + * The book manager. */ - public function __construct(array $configuration, $plugin_id, array $plugin_definition, Request $request) { + public function __construct(array $configuration, $plugin_id, array $plugin_definition, Request $request, BookManagerInterface $book_manager) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->request = $request; + $this->bookManager = $book_manager; } /** @@ -56,7 +67,8 @@ public static function create(ContainerInterface $container, array $configuratio $configuration, $plugin_id, $plugin_definition, - $container->get('request') + $container->get('request'), + $container->get('book.manager') ); } @@ -107,12 +119,12 @@ public function build() { if ($this->configuration['block_mode'] == 'all pages') { $book_menus = array(); $pseudo_tree = array(0 => array('below' => FALSE)); - foreach (book_get_books() as $book_id => $book) { + foreach ($this->bookManager->getAllBooks() as $book_id => $book) { if ($book['bid'] == $current_bid) { // If the current page is a node associated with a book, the menu // needs to be retrieved. - $data = \Drupal::service('book.manager')->bookTreeAllData($node->book['bid'], $node->book); - $book_menus[$book_id] = \Drupal::service('book.manager')->bookTreeOutput($data); + $data = $this->bookManager->bookTreeAllData($node->book['bid'], $node->book); + $book_menus[$book_id] = $this->bookManager->bookTreeOutput($data); } else { // Since we know we will only display a link to the top node, there @@ -122,7 +134,7 @@ public function build() { $book_node = node_load($book['nid']); $book['access'] = $book_node->access('view'); $pseudo_tree[0]['link'] = $book; - $book_menus[$book_id] = \Drupal::service('book.manager')->bookTreeOutput($pseudo_tree); + $book_menus[$book_id] = $this->bookManager->bookTreeOutput($pseudo_tree); } } if ($book_menus) { @@ -140,10 +152,10 @@ public function build() { $nid = $select->execute()->fetchField(); // Only show the block if the user has view access for the top-level node. if ($nid) { - $tree = \Drupal::service('book.manager')->bookTreeAllData($node->book['bid'], $node->book); + $tree = $this->bookManager->bookTreeAllData($node->book['bid'], $node->book); // There should only be one element at the top level. $data = array_shift($tree); - $below = \Drupal::service('book.manager')->bookTreeOutput($data['below']); + $below = $this->bookManager->bookTreeOutput($data['below']); if (!empty($below)) { return $below; }