diff --git a/core/modules/book/book.services.yml b/core/modules/book/book.services.yml index 8dc0ec4..a2977f8 100644 --- a/core/modules/book/book.services.yml +++ b/core/modules/book/book.services.yml @@ -6,14 +6,16 @@ services: - { name: breadcrumb_builder, priority: 701 } book.manager: class: Drupal\book\BookManager - arguments: ['@database', '@entity.manager', '@string_translation', '@config.factory'] + arguments: ['@database', '@entity.manager', '@string_translation', '@config.factory', '@book.storage'] book.outline: class: Drupal\book\BookOutline arguments: ['@book.manager'] book.export: class: Drupal\book\BookExport arguments: ['@entity.manager', '@book.manager'] - + book.storage: + class: Drupal\book\BookStorage + arguments: ['@database'] access_check.book.removable: class: Drupal\book\Access\BookNodeIsRemovableAccessCheck arguments: ['@book.manager'] diff --git a/core/modules/book/src/BookManager.php b/core/modules/book/src/BookManager.php index c491cf4..e3504d9 100644 --- a/core/modules/book/src/BookManager.php +++ b/core/modules/book/src/BookManager.php @@ -57,6 +57,13 @@ class BookManager implements BookManagerInterface { protected $books; /** + * Book Storage. + * + * @var \Drupal\book\BookStorageInterface + */ + protected $bookStorage; + + /** * Stores flattened book trees. * * @var array @@ -66,11 +73,12 @@ class BookManager implements BookManagerInterface { /** * Constructs a BookManager object. */ - public function __construct(Connection $connection, EntityManagerInterface $entity_manager, TranslationInterface $translation, ConfigFactoryInterface $config_factory) { + public function __construct(Connection $connection, EntityManagerInterface $entity_manager, TranslationInterface $translation, ConfigFactoryInterface $config_factory, BookStorageInterface $book_storage) { $this->connection = $connection; $this->entityManager = $entity_manager; $this->stringTranslation = $translation; $this->configFactory = $config_factory; + $this->bookStorage = $book_storage; } /** @@ -88,16 +96,10 @@ public function getAllBooks() { */ protected function loadBooks() { $this->books = array(); - $nids = $this->connection->query("SELECT DISTINCT(bid) FROM {book}")->fetchCol(); + $nids = $this->bookStorage->getBooks(); if ($nids) { - $query = $this->connection->select('book', 'b', array('fetch' => \PDO::FETCH_ASSOC)); - $query->fields('b'); - $query->condition('b.nid', $nids); - $query->addTag('node_access'); - $query->addMetaData('base_table', 'book'); - $book_links = $query->execute(); - + $book_links = $this->bookStorage->loadMultiple($nids); $nodes = $this->entityManager->getStorage('node')->loadMultiple($nids); // @todo: Sort by weight and translated title. @@ -147,21 +149,7 @@ public function getParentDepthLimit(array $book_link) { * the passed book link. */ protected function findChildrenRelativeDepth(array $book_link) { - $query = $this->connection->select('book'); - $query->addField('book', 'depth'); - $query->condition('bid', $book_link['bid']); - $query->orderBy('depth', 'DESC'); - $query->range(0, 1); - - $i = 1; - $p = 'p1'; - while ($i <= static::BOOK_MAX_DEPTH && $book_link[$p]) { - $query->condition($p, $book_link[$p]); - $p = 'p' . ++$i; - } - - $max_depth = $query->execute()->fetchField(); - + $max_depth = $this->bookStorage->getChildRelativeDepth($book_link, static::BOOK_MAX_DEPTH); return ($max_depth > $book_link['depth']) ? $max_depth - $book_link['depth'] : 0; } @@ -435,14 +423,12 @@ public function getTableOfContents($bid, $depth_limit, array $exclude = array()) */ public function deleteFromBook($nid) { $original = $this->loadBookLink($nid, FALSE); - $this->connection->delete('book') - ->condition('nid', $nid) - ->execute(); + $this->bookStorage->delete($nid); + if ($nid == $original['bid']) { // Handle deletion of a top-level post. - $result = $this->connection->query("SELECT * FROM {book} WHERE pid = :nid", array( - ':nid' => $nid - ))->fetchAllAssoc('nid', \PDO::FETCH_ASSOC); + $result = $this->bookStorage->loadBookChildren($nid); + foreach ($result as $child) { $child['bid'] = $child['nid']; $this->updateOutline($child); @@ -629,32 +615,11 @@ protected function doBookTreeBuild($bid, array $parameters = array()) { } if (!isset($trees[$tree_cid])) { - $query = $this->connection->select('book'); - $query->fields('book'); - for ($i = 1; $i <= static::BOOK_MAX_DEPTH; $i++) { - $query->orderBy('p' . $i, 'ASC'); - } - $query->condition('bid', $bid); - if (!empty($parameters['expanded'])) { - $query->condition('pid', $parameters['expanded'], 'IN'); - } $min_depth = (isset($parameters['min_depth']) ? $parameters['min_depth'] : 1); - if ($min_depth != 1) { - $query->condition('depth', $min_depth, '>='); - } - if (isset($parameters['max_depth'])) { - $query->condition('depth', $parameters['max_depth'], '<='); - } - // Add custom query conditions, if any were passed. - if (isset($parameters['conditions'])) { - foreach ($parameters['conditions'] as $column => $value) { - $query->condition($column, $value); - } - } + $result = $this->bookStorage->getBookMenuTree($bid, $parameters, $min_depth, static::BOOK_MAX_DEPTH); // Build an ordered array of links using the query result object. $links = array(); - $result = $query->execute(); foreach ($result as $link) { $link = (array) $link; $links[$link['nid']] = $link; @@ -733,7 +698,7 @@ public function loadBookLink($nid, $translate = TRUE) { * {@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)); + $result = $this->bookStorage->loadMultiple($nids); $links = array(); foreach ($result as $link) { if ($translate) { @@ -754,14 +719,9 @@ public function saveBookLink(array $link, $new) { $link += $this->getLinkDefaults($link['nid']); if ($new) { // Insert new. - $this->connection->insert('book') - ->fields(array( - 'nid' => $link['nid'], - 'bid' => $link['bid'], - 'pid' => $link['pid'], - 'weight' => $link['weight'], - ) + $this->getBookParents($link, (array) $this->loadBookLink($link['pid'], FALSE))) - ->execute(); + $parents = $this->getBookParents($link, (array) $this->loadBookLink($link['pid'], FALSE)); + $this->bookStorage->insert($link, $parents); + // Update the has_children status of the parent. $this->updateParent($link); } @@ -796,10 +756,11 @@ public function saveBookLink(array $link, $new) { $this->updateParent($link); } // Update the weight and pid. - $query = $this->connection->update('book'); - $query->fields(array('weight' => $link['weight'], 'pid' => $link['pid'], 'bid' => $link['bid'])); - $query->condition('nid', $link['nid']); - $query->execute(); + $this->bookStorage->update($link['nid'], array( + 'weight' => $link['weight'], + 'pid' => $link['pid'], + 'bid' => $link['bid'], + )); } foreach ($affected_bids as $bid) { \Drupal::cache('data')->deleteTags(array('bid' => $bid)); @@ -816,10 +777,6 @@ public function saveBookLink(array $link, $new) { * The original parent of $link. */ protected function moveChildren(array $link, array $original) { - $query = $this->connection->update('book'); - - $query->fields(array('bid' => $link['bid'])); - $p = 'p1'; $expressions = array(); for ($i = 1; $i <= $link['depth']; $p = 'p' . ++$i) { @@ -841,18 +798,8 @@ protected function moveChildren(array $link, array $original) { // @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('bid', $original['bid']); - $p = 'p1'; - for ($i = 1; !empty($original[$p]); $p = 'p' . ++$i) { - $query->condition($p, $original[$p]); - } - - $query->execute(); + $this->bookStorage->updateMovedChildren($link['bid'], $original, $expressions, $shift); } /** @@ -874,10 +821,7 @@ protected function updateParent(array $link) { // Nothing to update. return TRUE; } - $query = $this->connection->update('book'); - $query->fields(array('has_children' => 1)) - ->condition('nid', $link['pid']); - return $query->execute(); + return $this->bookStorage->update($link['pid'], array('has_children' => 1)); } /** @@ -900,22 +844,13 @@ protected function updateOriginalParent(array $original) { return TRUE; } // Check if $original had at least one child. - $original_number_of_children = $this->connection->select('book', 'b') - ->condition('bid', $original['bid']) - ->condition('pid', $original['pid']) - ->condition('nid', $original['nid'], '<>') - ->countQuery() - ->execute() - ->fetchField(); + $original_number_of_children = $this->bookStorage->countOriginalLinkChildren($original); $parent_has_children = ((bool) $original_number_of_children) ? 1 : 0; // Update the parent. If the original link did not have children, then the // parent now does not have children. If the original had children, then the // the parent has children now (still). - $query = $this->connection->update('book'); - $query->fields(array('has_children' => $parent_has_children)) - ->condition('nid', $original['pid']); - return $query->execute(); + return $this->bookStorage->update($original['pid'], array('has_children' => $parent_has_children)); } /** @@ -948,15 +883,14 @@ public function bookTreeCheckAccess(&$tree, $node_links = array()) { if ($node_links) { // @todo Extract that into its own method. $nids = array_keys($node_links); - $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. - $select->condition('n.status', 1); + $nids = \Drupal::entityQuery('node') + ->condition('nid', $nids) + ->condition('status', 1) + ->execute(); - $select->condition('n.nid', $nids, 'IN'); - $select->addTag('node_access'); - $nids = $select->execute()->fetchCol(); foreach ($nids as $nid) { foreach ($node_links[$nid] as $mlid => $link) { $node_links[$nid][$mlid]['access'] = TRUE; @@ -1102,17 +1036,9 @@ public function bookSubtreeData($link) { // If the subtree data was not in the cache, $data will be NULL. if (!isset($data)) { - $query = db_select('book', 'b', array('fetch' => \PDO::FETCH_ASSOC)); - $query->fields('b'); - $query->condition('b.bid', $link['bid']); - for ($i = 1; $i <= static::BOOK_MAX_DEPTH && $link["p$i"]; ++$i) { - $query->condition("p$i", $link["p$i"]); - } - for ($i = 1; $i <= static::BOOK_MAX_DEPTH; ++$i) { - $query->orderBy("p$i"); - } + $result = $this->bookStorage->getBookSubtree($link, static::BOOK_MAX_DEPTH); $links = array(); - foreach ($query->execute() as $item) { + foreach ($result as $item) { $links[] = $item; } $data['tree'] = $this->buildBookOutlineData($links, array(), $link['depth']); diff --git a/core/modules/book/src/BookStorage.php b/core/modules/book/src/BookStorage.php new file mode 100644 index 0000000..621932c --- /dev/null +++ b/core/modules/book/src/BookStorage.php @@ -0,0 +1,193 @@ +connection = $connection; + } + + /** + * {@inheritdoc} + */ + public function getBooks() { + return $this->connection->query("SELECT DISTINCT(bid) FROM {book}")->fetchCol(); + } + + /** + * {@inheritdoc} + */ + public function loadMultiple($nids) { + $query = $this->connection->select('book', 'b', array('fetch' => \PDO::FETCH_ASSOC)); + $query->fields('b'); + $query->condition('b.nid', $nids); + $query->addTag('node_access'); + $query->addMetaData('base_table', 'book'); + return $query->execute(); + } + + /** + * {@inheritdoc} + */ + public function getChildRelativeDepth($book_link, $max_depth) { + $query = $this->connection->select('book'); + $query->addField('book', 'depth'); + $query->condition('bid', $book_link['bid']); + $query->orderBy('depth', 'DESC'); + $query->range(0, 1); + + $i = 1; + $p = 'p1'; + while ($i <= $max_depth && $book_link[$p]) { + $query->condition($p, $book_link[$p]); + $p = 'p' . ++$i; + } + + return $query->execute()->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function delete($nid) { + return $this->connection->delete('book') + ->condition('nid', $nid) + ->execute(); + } + + /** + * {@inheritdoc} + */ + public function loadBookChildren($pid) { + return $this->connection + ->query("SELECT * FROM {book} WHERE pid = :pid", array(':pid' => $pid)) + ->fetchAllAssoc('nid', \PDO::FETCH_ASSOC); + } + + /** + * {@inheritdoc} + */ + public function getBookMenuTree($bid, $parameters, $min_depth, $max_depth) { + $query = $this->connection->select('book'); + $query->fields('book'); + for ($i = 1; $i <= $max_depth; $i++) { + $query->orderBy('p' . $i, 'ASC'); + } + $query->condition('bid', $bid); + if (!empty($parameters['expanded'])) { + $query->condition('pid', $parameters['expanded'], 'IN'); + } + if ($min_depth != 1) { + $query->condition('depth', $min_depth, '>='); + } + if (isset($parameters['max_depth'])) { + $query->condition('depth', $parameters['max_depth'], '<='); + } + // Add custom query conditions, if any were passed. + if (isset($parameters['conditions'])) { + foreach ($parameters['conditions'] as $column => $value) { + $query->condition($column, $value); + } + } + + return $query->execute(); + } + + /** + * {@inheritdoc} + */ + public function insert($link, $parents) { + return $this->connection + ->insert('book') + ->fields(array( + 'nid' => $link['nid'], + 'bid' => $link['bid'], + 'pid' => $link['pid'], + 'weight' => $link['weight'], + ) + $parents + ) + ->execute(); + } + + /** + * {@inheritdoc} + */ + public function update($nid, $fields) { + return $this->connection + ->update('book') + ->fields($fields) + ->condition('nid', $nid) + ->execute(); + } + + /** + * {@inheritdoc} + */ + public function updateMovedChildren($bid, $original, $expressions, $shift) { + $query = $this->connection->update('book'); + $query->fields(array('bid' => $bid)); + + foreach ($expressions as $expression) { + $query->expression($expression[0], $expression[1], $expression[2]); + } + + $query->expression('depth', 'depth + :depth', array(':depth' => $shift)); + $query->condition('bid', $original['bid']); + $p = 'p1'; + for ($i = 1; !empty($original[$p]); $p = 'p' . ++$i) { + $query->condition($p, $original[$p]); + } + + return $query->execute(); + } + + /** + * {@inheritdoc} + */ + public function countOriginalLinkChildren($original) { + return $this->connection->select('book', 'b') + ->condition('bid', $original['bid']) + ->condition('pid', $original['pid']) + ->condition('nid', $original['nid'], '<>') + ->countQuery() + ->execute()->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function getBookSubtree($link, $max_depth) { + $query = db_select('book', 'b', array('fetch' => \PDO::FETCH_ASSOC)); + $query->fields('b'); + $query->condition('b.bid', $link['bid']); + + for ($i = 1; $i <= $max_depth && $link["p$i"]; ++$i) { + $query->condition("p$i", $link["p$i"]); + } + for ($i = 1; $i <= $max_depth; ++$i) { + $query->orderBy("p$i"); + } + return $query->execute(); + } +} diff --git a/core/modules/book/src/BookStorageInterface.php b/core/modules/book/src/BookStorageInterface.php new file mode 100644 index 0000000..ea03893 --- /dev/null +++ b/core/modules/book/src/BookStorageInterface.php @@ -0,0 +1,156 @@ +fields('n', array('nid')) - ->condition('n.nid', $node->book['bid']) - ->addTag('node_access'); - $nid = $select->execute()->fetchField(); + $query = \Drupal::entityQuery('node'); + $nid = $query->condition('nid', $node->book['bid'], '=')->execute(); + // Only show the block if the user has view access for the top-level node. if ($nid) { $tree = $this->bookManager->bookTreeAllData($node->book['bid'], $node->book); diff --git a/core/modules/book/tests/src/BookManagerTest.php b/core/modules/book/tests/src/BookManagerTest.php index 4abf063..a7e0964 100644 --- a/core/modules/book/tests/src/BookManagerTest.php +++ b/core/modules/book/tests/src/BookManagerTest.php @@ -52,6 +52,13 @@ class BookManagerTest extends UnitTestCase { protected $bookManager; /** + * Book Storage. + * + * @var \Drupal\book\BookStorageInterface + */ + protected $bookStorage; + + /** * {@inheritdoc} */ protected function setUp() { @@ -61,7 +68,8 @@ protected function setUp() { $this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface'); $this->translation = $this->getStringTranslationStub(); $this->configFactory = $this->getConfigFactoryStub(array()); - $this->bookManager = new BookManager($this->connection, $this->entityManager, $this->translation, $this->configFactory); + $this->bookStorage = $this->getMock('Drupal\book\BookStorageInterface'); + $this->bookManager = new BookManager($this->connection, $this->entityManager, $this->translation, $this->configFactory, $this->bookStorage); } /**