Commit 0da52046 authored by catch's avatar catch
Browse files

Issue #2209025 by dawehner, fgm, pwolanin: Phase 3 - Make the BookManager...

Issue #2209025 by dawehner, fgm, pwolanin: Phase 3 - Make the BookManager interface more coherent, move more code out of book.module.
parent a6658cdd
......@@ -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',
......
......@@ -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
......
......@@ -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',
......
......@@ -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;
}