PageRenderTime 49ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/core/modules/book/src/BookManager.php

http://github.com/drupal/drupal
PHP | 1162 lines | 619 code | 100 blank | 443 comment | 114 complexity | 8f2b7d16a697f332335999be0ad94b68 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1
  1. <?php
  2. namespace Drupal\book;
  3. use Drupal\Component\Utility\Unicode;
  4. use Drupal\Core\Cache\Cache;
  5. use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
  6. use Drupal\Core\Entity\EntityTypeManagerInterface;
  7. use Drupal\Core\Form\FormStateInterface;
  8. use Drupal\Core\Render\RendererInterface;
  9. use Drupal\Core\Session\AccountInterface;
  10. use Drupal\Core\StringTranslation\TranslationInterface;
  11. use Drupal\Core\StringTranslation\StringTranslationTrait;
  12. use Drupal\Core\Config\ConfigFactoryInterface;
  13. use Drupal\Core\Template\Attribute;
  14. use Drupal\node\NodeInterface;
  15. /**
  16. * Defines a book manager.
  17. */
  18. class BookManager implements BookManagerInterface {
  19. use StringTranslationTrait;
  20. use DeprecatedServicePropertyTrait;
  21. /**
  22. * {@inheritdoc}
  23. */
  24. protected $deprecatedProperties = ['entityManager' => 'entity.manager'];
  25. /**
  26. * Defines the maximum supported depth of the book tree.
  27. */
  28. const BOOK_MAX_DEPTH = 9;
  29. /**
  30. * Entity type manager.
  31. *
  32. * @var \Drupal\Core\Entity\EntityTypeManagerInterface
  33. */
  34. protected $entityTypeManager;
  35. /**
  36. * Config Factory Service Object.
  37. *
  38. * @var \Drupal\Core\Config\ConfigFactoryInterface
  39. */
  40. protected $configFactory;
  41. /**
  42. * Books Array.
  43. *
  44. * @var array
  45. */
  46. protected $books;
  47. /**
  48. * Book outline storage.
  49. *
  50. * @var \Drupal\book\BookOutlineStorageInterface
  51. */
  52. protected $bookOutlineStorage;
  53. /**
  54. * Stores flattened book trees.
  55. *
  56. * @var array
  57. */
  58. protected $bookTreeFlattened;
  59. /**
  60. * The renderer.
  61. *
  62. * @var \Drupal\Core\Render\RendererInterface
  63. */
  64. protected $renderer;
  65. /**
  66. * Constructs a BookManager object.
  67. *
  68. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
  69. * The entity type manager.
  70. * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
  71. * The string translation service.
  72. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
  73. * The config factory.
  74. * @param \Drupal\book\BookOutlineStorageInterface $book_outline_storage
  75. * The book outline storage.
  76. * @param \Drupal\Core\Render\RendererInterface $renderer
  77. * The renderer.
  78. */
  79. public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $translation, ConfigFactoryInterface $config_factory, BookOutlineStorageInterface $book_outline_storage, RendererInterface $renderer) {
  80. $this->entityTypeManager = $entity_type_manager;
  81. $this->stringTranslation = $translation;
  82. $this->configFactory = $config_factory;
  83. $this->bookOutlineStorage = $book_outline_storage;
  84. $this->renderer = $renderer;
  85. }
  86. /**
  87. * {@inheritdoc}
  88. */
  89. public function getAllBooks() {
  90. if (!isset($this->books)) {
  91. $this->loadBooks();
  92. }
  93. return $this->books;
  94. }
  95. /**
  96. * Loads Books Array.
  97. */
  98. protected function loadBooks() {
  99. $this->books = [];
  100. $nids = $this->bookOutlineStorage->getBooks();
  101. if ($nids) {
  102. $book_links = $this->bookOutlineStorage->loadMultiple($nids);
  103. $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($nids);
  104. // @todo: Sort by weight and translated title.
  105. // @todo: use route name for links, not system path.
  106. foreach ($book_links as $link) {
  107. $nid = $link['nid'];
  108. if (isset($nodes[$nid]) && $nodes[$nid]->status) {
  109. $link['url'] = $nodes[$nid]->toUrl();
  110. $link['title'] = $nodes[$nid]->label();
  111. $link['type'] = $nodes[$nid]->bundle();
  112. $this->books[$link['bid']] = $link;
  113. }
  114. }
  115. }
  116. }
  117. /**
  118. * {@inheritdoc}
  119. */
  120. public function getLinkDefaults($nid) {
  121. return [
  122. 'original_bid' => 0,
  123. 'nid' => $nid,
  124. 'bid' => 0,
  125. 'pid' => 0,
  126. 'has_children' => 0,
  127. 'weight' => 0,
  128. 'options' => [],
  129. ];
  130. }
  131. /**
  132. * {@inheritdoc}
  133. */
  134. public function getParentDepthLimit(array $book_link) {
  135. return static::BOOK_MAX_DEPTH - 1 - (($book_link['bid'] && $book_link['has_children']) ? $this->findChildrenRelativeDepth($book_link) : 0);
  136. }
  137. /**
  138. * Determine the relative depth of the children of a given book link.
  139. *
  140. * @param array $book_link
  141. * The book link.
  142. *
  143. * @return int
  144. * The difference between the max depth in the book tree and the depth of
  145. * the passed book link.
  146. */
  147. protected function findChildrenRelativeDepth(array $book_link) {
  148. $max_depth = $this->bookOutlineStorage->getChildRelativeDepth($book_link, static::BOOK_MAX_DEPTH);
  149. return ($max_depth > $book_link['depth']) ? $max_depth - $book_link['depth'] : 0;
  150. }
  151. /**
  152. * {@inheritdoc}
  153. */
  154. public function addFormElements(array $form, FormStateInterface $form_state, NodeInterface $node, AccountInterface $account, $collapsed = TRUE) {
  155. // If the form is being processed during the Ajax callback of our book bid
  156. // dropdown, then $form_state will hold the value that was selected.
  157. if ($form_state->hasValue('book')) {
  158. $node->book = $form_state->getValue('book');
  159. }
  160. $form['book'] = [
  161. '#type' => 'details',
  162. '#title' => $this->t('Book outline'),
  163. '#weight' => 10,
  164. '#open' => !$collapsed,
  165. '#group' => 'advanced',
  166. '#attributes' => [
  167. 'class' => ['book-outline-form'],
  168. ],
  169. '#attached' => [
  170. 'library' => ['book/drupal.book'],
  171. ],
  172. '#tree' => TRUE,
  173. ];
  174. foreach (['nid', 'has_children', 'original_bid', 'parent_depth_limit'] as $key) {
  175. $form['book'][$key] = [
  176. '#type' => 'value',
  177. '#value' => $node->book[$key],
  178. ];
  179. }
  180. $form['book']['pid'] = $this->addParentSelectFormElements($node->book);
  181. // @see \Drupal\book\Form\BookAdminEditForm::bookAdminTableTree(). The
  182. // weight may be larger than 15.
  183. $form['book']['weight'] = [
  184. '#type' => 'weight',
  185. '#title' => $this->t('Weight'),
  186. '#default_value' => $node->book['weight'],
  187. '#delta' => max(15, abs($node->book['weight'])),
  188. '#weight' => 5,
  189. '#description' => $this->t('Pages at a given level are ordered first by weight and then by title.'),
  190. ];
  191. $options = [];
  192. $nid = !$node->isNew() ? $node->id() : 'new';
  193. if ($node->id() && ($nid == $node->book['original_bid']) && ($node->book['parent_depth_limit'] == 0)) {
  194. // This is the top level node in a maximum depth book and thus cannot be
  195. // moved.
  196. $options[$node->id()] = $node->label();
  197. }
  198. else {
  199. foreach ($this->getAllBooks() as $book) {
  200. $options[$book['nid']] = $book['title'];
  201. }
  202. }
  203. if ($account->hasPermission('create new books') && ($nid == 'new' || ($nid != $node->book['original_bid']))) {
  204. // The node can become a new book, if it is not one already.
  205. $options = [$nid => $this->t('- Create a new book -')] + $options;
  206. }
  207. if (!$node->book['bid']) {
  208. // The node is not currently in the hierarchy.
  209. $options = [0 => $this->t('- None -')] + $options;
  210. }
  211. // Add a drop-down to select the destination book.
  212. $form['book']['bid'] = [
  213. '#type' => 'select',
  214. '#title' => $this->t('Book'),
  215. '#default_value' => $node->book['bid'],
  216. '#options' => $options,
  217. '#access' => (bool) $options,
  218. '#description' => $this->t('Your page will be a part of the selected book.'),
  219. '#weight' => -5,
  220. '#attributes' => ['class' => ['book-title-select']],
  221. '#ajax' => [
  222. 'callback' => 'book_form_update',
  223. 'wrapper' => 'edit-book-plid-wrapper',
  224. 'effect' => 'fade',
  225. 'speed' => 'fast',
  226. ],
  227. ];
  228. return $form;
  229. }
  230. /**
  231. * {@inheritdoc}
  232. */
  233. public function checkNodeIsRemovable(NodeInterface $node) {
  234. return (!empty($node->book['bid']) && (($node->book['bid'] != $node->id()) || !$node->book['has_children']));
  235. }
  236. /**
  237. * {@inheritdoc}
  238. */
  239. public function updateOutline(NodeInterface $node) {
  240. if (empty($node->book['bid'])) {
  241. return FALSE;
  242. }
  243. if (!empty($node->book['bid'])) {
  244. if ($node->book['bid'] == 'new') {
  245. // New nodes that are their own book.
  246. $node->book['bid'] = $node->id();
  247. }
  248. elseif (!isset($node->book['original_bid'])) {
  249. $node->book['original_bid'] = $node->book['bid'];
  250. }
  251. }
  252. // Ensure we create a new book link if either the node itself is new, or the
  253. // bid was selected the first time, so that the original_bid is still empty.
  254. $new = empty($node->book['nid']) || empty($node->book['original_bid']);
  255. $node->book['nid'] = $node->id();
  256. // Create a new book from a node.
  257. if ($node->book['bid'] == $node->id()) {
  258. $node->book['pid'] = 0;
  259. }
  260. elseif ($node->book['pid'] < 0) {
  261. // -1 is the default value in BookManager::addParentSelectFormElements().
  262. // The node save should have set the bid equal to the node ID, but
  263. // handle it here if it did not.
  264. $node->book['pid'] = $node->book['bid'];
  265. }
  266. // Prevent changes to the book outline if the node being saved is not the
  267. // default revision.
  268. $updated = FALSE;
  269. if (!$new) {
  270. $original = $this->loadBookLink($node->id(), FALSE);
  271. if ($node->book['bid'] != $original['bid'] || $node->book['pid'] != $original['pid'] || $node->book['weight'] != $original['weight']) {
  272. $updated = TRUE;
  273. }
  274. }
  275. if (($new || $updated) && !$node->isDefaultRevision()) {
  276. return FALSE;
  277. }
  278. return $this->saveBookLink($node->book, $new);
  279. }
  280. /**
  281. * {@inheritdoc}
  282. */
  283. public function getBookParents(array $item, array $parent = []) {
  284. $book = [];
  285. if ($item['pid'] == 0) {
  286. $book['p1'] = $item['nid'];
  287. for ($i = 2; $i <= static::BOOK_MAX_DEPTH; $i++) {
  288. $parent_property = "p$i";
  289. $book[$parent_property] = 0;
  290. }
  291. $book['depth'] = 1;
  292. }
  293. else {
  294. $i = 1;
  295. $book['depth'] = $parent['depth'] + 1;
  296. while ($i < $book['depth']) {
  297. $p = 'p' . $i++;
  298. $book[$p] = $parent[$p];
  299. }
  300. $p = 'p' . $i++;
  301. // The parent (p1 - p9) corresponding to the depth always equals the nid.
  302. $book[$p] = $item['nid'];
  303. while ($i <= static::BOOK_MAX_DEPTH) {
  304. $p = 'p' . $i++;
  305. $book[$p] = 0;
  306. }
  307. }
  308. return $book;
  309. }
  310. /**
  311. * Builds the parent selection form element for the node form or outline tab.
  312. *
  313. * This function is also called when generating a new set of options during
  314. * the Ajax callback, so an array is returned that can be used to replace an
  315. * existing form element.
  316. *
  317. * @param array $book_link
  318. * A fully loaded book link that is part of the book hierarchy.
  319. *
  320. * @return array
  321. * A parent selection form element.
  322. */
  323. protected function addParentSelectFormElements(array $book_link) {
  324. $config = $this->configFactory->get('book.settings');
  325. if ($config->get('override_parent_selector')) {
  326. return [];
  327. }
  328. // Offer a message or a drop-down to choose a different parent page.
  329. $form = [
  330. '#type' => 'hidden',
  331. '#value' => -1,
  332. '#prefix' => '<div id="edit-book-plid-wrapper">',
  333. '#suffix' => '</div>',
  334. ];
  335. if ($book_link['nid'] === $book_link['bid']) {
  336. // This is a book - at the top level.
  337. if ($book_link['original_bid'] === $book_link['bid']) {
  338. $form['#prefix'] .= '<em>' . $this->t('This is the top-level page in this book.') . '</em>';
  339. }
  340. else {
  341. $form['#prefix'] .= '<em>' . $this->t('This will be the top-level page in this book.') . '</em>';
  342. }
  343. }
  344. elseif (!$book_link['bid']) {
  345. $form['#prefix'] .= '<em>' . $this->t('No book selected.') . '</em>';
  346. }
  347. else {
  348. $form = [
  349. '#type' => 'select',
  350. '#title' => $this->t('Parent item'),
  351. '#default_value' => $book_link['pid'],
  352. '#description' => $this->t('The parent page in the book. The maximum depth for a book and all child pages is @maxdepth. Some pages in the selected book may not be available as parents if selecting them would exceed this limit.', ['@maxdepth' => static::BOOK_MAX_DEPTH]),
  353. '#options' => $this->getTableOfContents($book_link['bid'], $book_link['parent_depth_limit'], [$book_link['nid']]),
  354. '#attributes' => ['class' => ['book-title-select']],
  355. '#prefix' => '<div id="edit-book-plid-wrapper">',
  356. '#suffix' => '</div>',
  357. ];
  358. }
  359. $this->renderer->addCacheableDependency($form, $config);
  360. return $form;
  361. }
  362. /**
  363. * Recursively processes and formats book links for getTableOfContents().
  364. *
  365. * This helper function recursively modifies the table of contents array for
  366. * each item in the book tree, ignoring items in the exclude array or at a
  367. * depth greater than the limit. Truncates titles over thirty characters and
  368. * appends an indentation string incremented by depth.
  369. *
  370. * @param array $tree
  371. * The data structure of the book's outline tree. Includes hidden links.
  372. * @param string $indent
  373. * A string appended to each node title. Increments by '--' per depth
  374. * level.
  375. * @param array $toc
  376. * Reference to the table of contents array. This is modified in place, so
  377. * the function does not have a return value.
  378. * @param array $exclude
  379. * Optional array of Node ID values. Any link whose node ID is in this
  380. * array will be excluded (along with its children).
  381. * @param int $depth_limit
  382. * Any link deeper than this value will be excluded (along with its
  383. * children).
  384. */
  385. protected function recurseTableOfContents(array $tree, $indent, array &$toc, array $exclude, $depth_limit) {
  386. $nids = [];
  387. foreach ($tree as $data) {
  388. if ($data['link']['depth'] > $depth_limit) {
  389. // Don't iterate through any links on this level.
  390. return;
  391. }
  392. if (!in_array($data['link']['nid'], $exclude)) {
  393. $nids[] = $data['link']['nid'];
  394. }
  395. }
  396. $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($nids);
  397. foreach ($tree as $data) {
  398. $nid = $data['link']['nid'];
  399. // Check for excluded or missing node.
  400. if (empty($nodes[$nid])) {
  401. continue;
  402. }
  403. $toc[$nid] = $indent . ' ' . Unicode::truncate($nodes[$nid]->label(), 30, TRUE, TRUE);
  404. if ($data['below']) {
  405. $this->recurseTableOfContents($data['below'], $indent . '--', $toc, $exclude, $depth_limit);
  406. }
  407. }
  408. }
  409. /**
  410. * {@inheritdoc}
  411. */
  412. public function getTableOfContents($bid, $depth_limit, array $exclude = []) {
  413. $tree = $this->bookTreeAllData($bid);
  414. $toc = [];
  415. $this->recurseTableOfContents($tree, '', $toc, $exclude, $depth_limit);
  416. return $toc;
  417. }
  418. /**
  419. * {@inheritdoc}
  420. */
  421. public function deleteFromBook($nid) {
  422. $original = $this->loadBookLink($nid, FALSE);
  423. $this->bookOutlineStorage->delete($nid);
  424. if ($nid == $original['bid']) {
  425. // Handle deletion of a top-level post.
  426. $result = $this->bookOutlineStorage->loadBookChildren($nid);
  427. $children = $this->entityTypeManager->getStorage('node')->loadMultiple(array_keys($result));
  428. foreach ($children as $child) {
  429. $child->book['bid'] = $child->id();
  430. $this->updateOutline($child);
  431. }
  432. }
  433. $this->updateOriginalParent($original);
  434. $this->books = NULL;
  435. Cache::invalidateTags(['bid:' . $original['bid']]);
  436. }
  437. /**
  438. * {@inheritdoc}
  439. */
  440. public function bookTreeAllData($bid, $link = NULL, $max_depth = NULL) {
  441. $tree = &drupal_static(__METHOD__, []);
  442. $language_interface = \Drupal::languageManager()->getCurrentLanguage();
  443. // Use $nid as a flag for whether the data being loaded is for the whole
  444. // tree.
  445. $nid = isset($link['nid']) ? $link['nid'] : 0;
  446. // Generate a cache ID (cid) specific for this $bid, $link, $language, and
  447. // depth.
  448. $cid = 'book-links:' . $bid . ':all:' . $nid . ':' . $language_interface->getId() . ':' . (int) $max_depth;
  449. if (!isset($tree[$cid])) {
  450. // If the tree data was not in the static cache, build $tree_parameters.
  451. $tree_parameters = [
  452. 'min_depth' => 1,
  453. 'max_depth' => $max_depth,
  454. ];
  455. if ($nid) {
  456. $active_trail = $this->getActiveTrailIds($bid, $link);
  457. $tree_parameters['expanded'] = $active_trail;
  458. $tree_parameters['active_trail'] = $active_trail;
  459. $tree_parameters['active_trail'][] = $nid;
  460. }
  461. // Build the tree using the parameters; the resulting tree will be cached.
  462. $tree[$cid] = $this->bookTreeBuild($bid, $tree_parameters);
  463. }
  464. return $tree[$cid];
  465. }
  466. /**
  467. * {@inheritdoc}
  468. */
  469. public function getActiveTrailIds($bid, $link) {
  470. // The tree is for a single item, so we need to match the values in its
  471. // p columns and 0 (the top level) with the plid values of other links.
  472. $active_trail = [0];
  473. for ($i = 1; $i < static::BOOK_MAX_DEPTH; $i++) {
  474. if (!empty($link["p$i"])) {
  475. $active_trail[] = $link["p$i"];
  476. }
  477. }
  478. return $active_trail;
  479. }
  480. /**
  481. * {@inheritdoc}
  482. */
  483. public function bookTreeOutput(array $tree) {
  484. $items = $this->buildItems($tree);
  485. $build = [];
  486. if ($items) {
  487. // Make sure drupal_render() does not re-order the links.
  488. $build['#sorted'] = TRUE;
  489. // Get the book id from the last link.
  490. $item = end($items);
  491. // Add the theme wrapper for outer markup.
  492. // Allow menu-specific theme overrides.
  493. $build['#theme'] = 'book_tree__book_toc_' . $item['original_link']['bid'];
  494. $build['#items'] = $items;
  495. // Set cache tag.
  496. $build['#cache']['tags'][] = 'config:system.book.' . $item['original_link']['bid'];
  497. }
  498. return $build;
  499. }
  500. /**
  501. * Builds the #items property for a book tree's renderable array.
  502. *
  503. * Helper function for ::bookTreeOutput().
  504. *
  505. * @param array $tree
  506. * A data structure representing the tree.
  507. *
  508. * @return array
  509. * The value to use for the #items property of a renderable menu.
  510. */
  511. protected function buildItems(array $tree) {
  512. $items = [];
  513. foreach ($tree as $data) {
  514. $element = [];
  515. // Generally we only deal with visible links, but just in case.
  516. if (!$data['link']['access']) {
  517. continue;
  518. }
  519. // Set a class for the <li> tag. Since $data['below'] may contain local
  520. // tasks, only set 'expanded' to true if the link also has children within
  521. // the current book.
  522. $element['is_expanded'] = FALSE;
  523. $element['is_collapsed'] = FALSE;
  524. if ($data['link']['has_children'] && $data['below']) {
  525. $element['is_expanded'] = TRUE;
  526. }
  527. elseif ($data['link']['has_children']) {
  528. $element['is_collapsed'] = TRUE;
  529. }
  530. // Set a helper variable to indicate whether the link is in the active
  531. // trail.
  532. $element['in_active_trail'] = FALSE;
  533. if ($data['link']['in_active_trail']) {
  534. $element['in_active_trail'] = TRUE;
  535. }
  536. // Allow book-specific theme overrides.
  537. $element['attributes'] = new Attribute();
  538. $element['title'] = $data['link']['title'];
  539. $node = $this->entityTypeManager->getStorage('node')->load($data['link']['nid']);
  540. $element['url'] = $node->toUrl();
  541. $element['localized_options'] = !empty($data['link']['localized_options']) ? $data['link']['localized_options'] : [];
  542. $element['localized_options']['set_active_class'] = TRUE;
  543. $element['below'] = $data['below'] ? $this->buildItems($data['below']) : [];
  544. $element['original_link'] = $data['link'];
  545. // Index using the link's unique nid.
  546. $items[$data['link']['nid']] = $element;
  547. }
  548. return $items;
  549. }
  550. /**
  551. * Builds a book tree, translates links, and checks access.
  552. *
  553. * @param int $bid
  554. * The Book ID to find links for.
  555. * @param array $parameters
  556. * (optional) An associative array of build parameters. Possible keys:
  557. * - expanded: An array of parent link IDs to return only book links that
  558. * are children of one of the parent link IDs in this list. If empty,
  559. * the whole outline is built, unless 'only_active_trail' is TRUE.
  560. * - active_trail: An array of node IDs, representing the currently active
  561. * book link.
  562. * - only_active_trail: Whether to only return links that are in the active
  563. * trail. This option is ignored if 'expanded' is non-empty.
  564. * - min_depth: The minimum depth of book links in the resulting tree.
  565. * Defaults to 1, which is to build the whole tree for the book.
  566. * - max_depth: The maximum depth of book links in the resulting tree.
  567. * - conditions: An associative array of custom database select query
  568. * condition key/value pairs; see
  569. * \Drupal\book\BookOutlineStorage::getBookMenuTree() for the actual
  570. * query.
  571. *
  572. * @return array
  573. * A fully built book tree.
  574. */
  575. protected function bookTreeBuild($bid, array $parameters = []) {
  576. // Build the book tree.
  577. $data = $this->doBookTreeBuild($bid, $parameters);
  578. // Check access for the current user to each item in the tree.
  579. $this->bookTreeCheckAccess($data['tree'], $data['node_links']);
  580. return $data['tree'];
  581. }
  582. /**
  583. * Builds a book tree.
  584. *
  585. * This function may be used build the data for a menu tree only, for example
  586. * to further massage the data manually before further processing happens.
  587. * _menu_tree_check_access() needs to be invoked afterwards.
  588. *
  589. * @param int $bid
  590. * The book ID to find links for.
  591. * @param array $parameters
  592. * (optional) An associative array of build parameters. Possible keys:
  593. * - expanded: An array of parent link IDs to return only book links that
  594. * are children of one of the parent link IDs in this list. If empty,
  595. * the whole outline is built, unless 'only_active_trail' is TRUE.
  596. * - active_trail: An array of node IDs, representing the currently active
  597. * book link.
  598. * - only_active_trail: Whether to only return links that are in the active
  599. * trail. This option is ignored if 'expanded' is non-empty.
  600. * - min_depth: The minimum depth of book links in the resulting tree.
  601. * Defaults to 1, which is to build the whole tree for the book.
  602. * - max_depth: The maximum depth of book links in the resulting tree.
  603. * - conditions: An associative array of custom database select query
  604. * condition key/value pairs; see
  605. * \Drupal\book\BookOutlineStorage::getBookMenuTree() for the actual
  606. * query.
  607. *
  608. * @return array
  609. * An array with links representing the tree structure of the book.
  610. *
  611. * @see \Drupal\book\BookOutlineStorageInterface::getBookMenuTree()
  612. */
  613. protected function doBookTreeBuild($bid, array $parameters = []) {
  614. // Static cache of already built menu trees.
  615. $trees = &drupal_static(__METHOD__, []);
  616. $language_interface = \Drupal::languageManager()->getCurrentLanguage();
  617. // Build the cache id; sort parents to prevent duplicate storage and remove
  618. // default parameter values.
  619. if (isset($parameters['expanded'])) {
  620. sort($parameters['expanded']);
  621. }
  622. $tree_cid = 'book-links:' . $bid . ':tree-data:' . $language_interface->getId() . ':' . hash('sha256', serialize($parameters));
  623. // If we do not have this tree in the static cache, check {cache_data}.
  624. if (!isset($trees[$tree_cid])) {
  625. $cache = \Drupal::cache('data')->get($tree_cid);
  626. if ($cache && $cache->data) {
  627. $trees[$tree_cid] = $cache->data;
  628. }
  629. }
  630. if (!isset($trees[$tree_cid])) {
  631. $min_depth = (isset($parameters['min_depth']) ? $parameters['min_depth'] : 1);
  632. $result = $this->bookOutlineStorage->getBookMenuTree($bid, $parameters, $min_depth, static::BOOK_MAX_DEPTH);
  633. // Build an ordered array of links using the query result object.
  634. $links = [];
  635. foreach ($result as $link) {
  636. $link = (array) $link;
  637. $links[$link['nid']] = $link;
  638. }
  639. $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : []);
  640. $data['tree'] = $this->buildBookOutlineData($links, $active_trail, $min_depth);
  641. $data['node_links'] = [];
  642. $this->bookTreeCollectNodeLinks($data['tree'], $data['node_links']);
  643. // Cache the data, if it is not already in the cache.
  644. \Drupal::cache('data')->set($tree_cid, $data, Cache::PERMANENT, ['bid:' . $bid]);
  645. $trees[$tree_cid] = $data;
  646. }
  647. return $trees[$tree_cid];
  648. }
  649. /**
  650. * {@inheritdoc}
  651. */
  652. public function bookTreeCollectNodeLinks(&$tree, &$node_links) {
  653. // All book links are nodes.
  654. // @todo clean this up.
  655. foreach ($tree as $key => $v) {
  656. $nid = $v['link']['nid'];
  657. $node_links[$nid][$tree[$key]['link']['nid']] = &$tree[$key]['link'];
  658. $tree[$key]['link']['access'] = FALSE;
  659. if ($tree[$key]['below']) {
  660. $this->bookTreeCollectNodeLinks($tree[$key]['below'], $node_links);
  661. }
  662. }
  663. }
  664. /**
  665. * {@inheritdoc}
  666. */
  667. public function bookTreeGetFlat(array $book_link) {
  668. if (!isset($this->bookTreeFlattened[$book_link['nid']])) {
  669. // Call $this->bookTreeAllData() to take advantage of caching.
  670. $tree = $this->bookTreeAllData($book_link['bid'], $book_link, $book_link['depth'] + 1);
  671. $this->bookTreeFlattened[$book_link['nid']] = [];
  672. $this->flatBookTree($tree, $this->bookTreeFlattened[$book_link['nid']]);
  673. }
  674. return $this->bookTreeFlattened[$book_link['nid']];
  675. }
  676. /**
  677. * Recursively converts a tree of menu links to a flat array.
  678. *
  679. * @param array $tree
  680. * A tree of menu links in an array.
  681. * @param array $flat
  682. * A flat array of the menu links from $tree, passed by reference.
  683. *
  684. * @see static::bookTreeGetFlat()
  685. */
  686. protected function flatBookTree(array $tree, array &$flat) {
  687. foreach ($tree as $data) {
  688. $flat[$data['link']['nid']] = $data['link'];
  689. if ($data['below']) {
  690. $this->flatBookTree($data['below'], $flat);
  691. }
  692. }
  693. }
  694. /**
  695. * {@inheritdoc}
  696. */
  697. public function loadBookLink($nid, $translate = TRUE) {
  698. $links = $this->loadBookLinks([$nid], $translate);
  699. return isset($links[$nid]) ? $links[$nid] : FALSE;
  700. }
  701. /**
  702. * {@inheritdoc}
  703. */
  704. public function loadBookLinks($nids, $translate = TRUE) {
  705. $result = $this->bookOutlineStorage->loadMultiple($nids, $translate);
  706. $links = [];
  707. foreach ($result as $link) {
  708. if ($translate) {
  709. $this->bookLinkTranslate($link);
  710. }
  711. $links[$link['nid']] = $link;
  712. }
  713. return $links;
  714. }
  715. /**
  716. * {@inheritdoc}
  717. */
  718. public function saveBookLink(array $link, $new) {
  719. // Keep track of Book IDs for cache clear.
  720. $affected_bids[$link['bid']] = $link['bid'];
  721. $link += $this->getLinkDefaults($link['nid']);
  722. if ($new) {
  723. // Insert new.
  724. $parents = $this->getBookParents($link, (array) $this->loadBookLink($link['pid'], FALSE));
  725. $this->bookOutlineStorage->insert($link, $parents);
  726. // Update the has_children status of the parent.
  727. $this->updateParent($link);
  728. }
  729. else {
  730. $original = $this->loadBookLink($link['nid'], FALSE);
  731. // Using the Book ID as the key keeps this unique.
  732. $affected_bids[$original['bid']] = $original['bid'];
  733. // Handle links that are moving.
  734. if ($link['bid'] != $original['bid'] || $link['pid'] != $original['pid']) {
  735. // Update the bid for this page and all children.
  736. if ($link['pid'] == 0) {
  737. $link['depth'] = 1;
  738. $parent = [];
  739. }
  740. // In case the form did not specify a proper PID we use the BID as new
  741. // parent.
  742. elseif (($parent_link = $this->loadBookLink($link['pid'], FALSE)) && $parent_link['bid'] != $link['bid']) {
  743. $link['pid'] = $link['bid'];
  744. $parent = $this->loadBookLink($link['pid'], FALSE);
  745. $link['depth'] = $parent['depth'] + 1;
  746. }
  747. else {
  748. $parent = $this->loadBookLink($link['pid'], FALSE);
  749. $link['depth'] = $parent['depth'] + 1;
  750. }
  751. $this->setParents($link, $parent);
  752. $this->moveChildren($link, $original);
  753. // Update the has_children status of the original parent.
  754. $this->updateOriginalParent($original);
  755. // Update the has_children status of the new parent.
  756. $this->updateParent($link);
  757. }
  758. // Update the weight and pid.
  759. $this->bookOutlineStorage->update($link['nid'], [
  760. 'weight' => $link['weight'],
  761. 'pid' => $link['pid'],
  762. 'bid' => $link['bid'],
  763. ]);
  764. }
  765. $cache_tags = [];
  766. foreach ($affected_bids as $bid) {
  767. $cache_tags[] = 'bid:' . $bid;
  768. }
  769. Cache::invalidateTags($cache_tags);
  770. return $link;
  771. }
  772. /**
  773. * Moves children from the original parent to the updated link.
  774. *
  775. * @param array $link
  776. * The link being saved.
  777. * @param array $original
  778. * The original parent of $link.
  779. */
  780. protected function moveChildren(array $link, array $original) {
  781. $p = 'p1';
  782. $expressions = [];
  783. for ($i = 1; $i <= $link['depth']; $p = 'p' . ++$i) {
  784. $expressions[] = [$p, ":p_$i", [":p_$i" => $link[$p]]];
  785. }
  786. $j = $original['depth'] + 1;
  787. while ($i <= static::BOOK_MAX_DEPTH && $j <= static::BOOK_MAX_DEPTH) {
  788. $expressions[] = ['p' . $i++, 'p' . $j++, []];
  789. }
  790. while ($i <= static::BOOK_MAX_DEPTH) {
  791. $expressions[] = ['p' . $i++, 0, []];
  792. }
  793. $shift = $link['depth'] - $original['depth'];
  794. if ($shift > 0) {
  795. // The order of expressions must be reversed so the new values don't
  796. // overwrite the old ones before they can be used because "Single-table
  797. // UPDATE assignments are generally evaluated from left to right"
  798. // @see http://dev.mysql.com/doc/refman/5.0/en/update.html
  799. $expressions = array_reverse($expressions);
  800. }
  801. $this->bookOutlineStorage->updateMovedChildren($link['bid'], $original, $expressions, $shift);
  802. }
  803. /**
  804. * Sets the has_children flag of the parent of the node.
  805. *
  806. * This method is mostly called when a book link is moved/created etc. So we
  807. * want to update the has_children flag of the new parent book link.
  808. *
  809. * @param array $link
  810. * The book link, data reflecting its new position, whose new parent we want
  811. * to update.
  812. *
  813. * @return bool
  814. * TRUE if the update was successful (either there is no parent to update,
  815. * or the parent was updated successfully), FALSE on failure.
  816. */
  817. protected function updateParent(array $link) {
  818. if ($link['pid'] == 0) {
  819. // Nothing to update.
  820. return TRUE;
  821. }
  822. return $this->bookOutlineStorage->update($link['pid'], ['has_children' => 1]);
  823. }
  824. /**
  825. * Updates the has_children flag of the parent of the original node.
  826. *
  827. * This method is called when a book link is moved or deleted. So we want to
  828. * update the has_children flag of the parent node.
  829. *
  830. * @param array $original
  831. * The original link whose parent we want to update.
  832. *
  833. * @return bool
  834. * TRUE if the update was successful (either there was no original parent to
  835. * update, or the original parent was updated successfully), FALSE on
  836. * failure.
  837. */
  838. protected function updateOriginalParent(array $original) {
  839. if ($original['pid'] == 0) {
  840. // There were no parents of this link. Nothing to update.
  841. return TRUE;
  842. }
  843. // Check if $original had at least one child.
  844. $original_number_of_children = $this->bookOutlineStorage->countOriginalLinkChildren($original);
  845. $parent_has_children = ((bool) $original_number_of_children) ? 1 : 0;
  846. // Update the parent. If the original link did not have children, then the
  847. // parent now does not have children. If the original had children, then the
  848. // the parent has children now (still).
  849. return $this->bookOutlineStorage->update($original['pid'], ['has_children' => $parent_has_children]);
  850. }
  851. /**
  852. * Sets the p1 through p9 properties for a book link being saved.
  853. *
  854. * @param array $link
  855. * The book link to update, passed by reference.
  856. * @param array $parent
  857. * The parent values to set.
  858. */
  859. protected function setParents(array &$link, array $parent) {
  860. $i = 1;
  861. while ($i < $link['depth']) {
  862. $p = 'p' . $i++;
  863. $link[$p] = $parent[$p];
  864. }
  865. $p = 'p' . $i++;
  866. // The parent (p1 - p9) corresponding to the depth always equals the nid.
  867. $link[$p] = $link['nid'];
  868. while ($i <= static::BOOK_MAX_DEPTH) {
  869. $p = 'p' . $i++;
  870. $link[$p] = 0;
  871. }
  872. }
  873. /**
  874. * {@inheritdoc}
  875. */
  876. public function bookTreeCheckAccess(&$tree, $node_links = []) {
  877. if ($node_links) {
  878. // @todo Extract that into its own method.
  879. $nids = array_keys($node_links);
  880. // @todo This should be actually filtering on the desired node status
  881. // field language and just fall back to the default language.
  882. $nids = \Drupal::entityQuery('node')
  883. ->condition('nid', $nids, 'IN')
  884. ->condition('status', 1)
  885. ->execute();
  886. foreach ($nids as $nid) {
  887. foreach ($node_links[$nid] as $mlid => $link) {
  888. $node_links[$nid][$mlid]['access'] = TRUE;
  889. }
  890. }
  891. }
  892. $this->doBookTreeCheckAccess($tree);
  893. }
  894. /**
  895. * Sorts the menu tree and recursively checks access for each item.
  896. *
  897. * @param array $tree
  898. * The book tree to operate on.
  899. */
  900. protected function doBookTreeCheckAccess(&$tree) {
  901. $new_tree = [];
  902. foreach ($tree as $key => $v) {
  903. $item = &$tree[$key]['link'];
  904. $this->bookLinkTranslate($item);
  905. if ($item['access']) {
  906. if ($tree[$key]['below']) {
  907. $this->doBookTreeCheckAccess($tree[$key]['below']);
  908. }
  909. // The weights are made a uniform 5 digits by adding 50000 as an offset.
  910. // After calling $this->bookLinkTranslate(), $item['title'] has the
  911. // translated title. Adding the nid to the end of the index insures that
  912. // it is unique.
  913. $new_tree[(50000 + $item['weight']) . ' ' . $item['title'] . ' ' . $item['nid']] = $tree[$key];
  914. }
  915. }
  916. // Sort siblings in the tree based on the weights and localized titles.
  917. ksort($new_tree);
  918. $tree = $new_tree;
  919. }
  920. /**
  921. * {@inheritdoc}
  922. */
  923. public function bookLinkTranslate(&$link) {
  924. $node = NULL;
  925. // Access will already be set in the tree functions.
  926. if (!isset($link['access'])) {
  927. $node = $this->entityTypeManager->getStorage('node')->load($link['nid']);
  928. $link['access'] = $node && $node->access('view');
  929. }
  930. // For performance, don't localize a link the user can't access.
  931. if ($link['access']) {
  932. // @todo - load the nodes en-mass rather than individually.
  933. if (!$node) {
  934. $node = $this->entityTypeManager->getStorage('node')
  935. ->load($link['nid']);
  936. }
  937. // The node label will be the value for the current user's language.
  938. $link['title'] = $node->label();
  939. $link['options'] = [];
  940. }
  941. return $link;
  942. }
  943. /**
  944. * Sorts and returns the built data representing a book tree.
  945. *
  946. * @param array $links
  947. * A flat array of book links that are part of the book. Each array element
  948. * is an associative array of information about the book link, containing
  949. * the fields from the {book} table. This array must be ordered depth-first.
  950. * @param array $parents
  951. * An array of the node ID values that are in the path from the current
  952. * page to the root of the book tree.
  953. * @param int $depth
  954. * The minimum depth to include in the returned book tree.
  955. *
  956. * @return array
  957. * An array of book links in the form of a tree. Each item in the tree is an
  958. * associative array containing:
  959. * - link: The book link item from $links, with additional element
  960. * 'in_active_trail' (TRUE if the link ID was in $parents).
  961. * - below: An array containing the sub-tree of this item, where each
  962. * element is a tree item array with 'link' and 'below' elements. This
  963. * array will be empty if the book link has no items in its sub-tree
  964. * having a depth greater than or equal to $depth.
  965. */
  966. protected function buildBookOutlineData(array $links, array $parents = [], $depth = 1) {
  967. // Reverse the array so we can use the more efficient array_pop() function.
  968. $links = array_reverse($links);
  969. return $this->buildBookOutlineRecursive($links, $parents, $depth);
  970. }
  971. /**
  972. * Builds the data representing a book tree.
  973. *
  974. * The function is a bit complex because the rendering of a link depends on
  975. * the next book link.
  976. *
  977. * @param array $links
  978. * A flat array of book links that are part of the book. Each array element
  979. * is an associative array of information about the book link, containing
  980. * the fields from the {book} table. This array must be ordered depth-first.
  981. * @param array $parents
  982. * An array of the node ID values that are in the path from the current page
  983. * to the root of the book tree.
  984. * @param int $depth
  985. * The minimum depth to include in the returned book tree.
  986. *
  987. * @return array
  988. * Book tree.
  989. */
  990. protected function buildBookOutlineRecursive(&$links, $parents, $depth) {
  991. $tree = [];
  992. while ($item = array_pop($links)) {
  993. // We need to determine if we're on the path to root so we can later build
  994. // the correct active trail.
  995. $item['in_active_trail'] = in_array($item['nid'], $parents);
  996. // Add the current link to the tree.
  997. $tree[$item['nid']] = [
  998. 'link' => $item,
  999. 'below' => [],
  1000. ];
  1001. // Look ahead to the next link, but leave it on the array so it's
  1002. // available to other recursive function calls if we return or build a
  1003. // sub-tree.
  1004. $next = end($links);
  1005. // Check whether the next link is the first in a new sub-tree.
  1006. if ($next && $next['depth'] > $depth) {
  1007. // Recursively call buildBookOutlineRecursive to build the sub-tree.
  1008. $tree[$item['nid']]['below'] = $this->buildBookOutlineRecursive($links, $parents, $next['depth']);
  1009. // Fetch next link after filling the sub-tree.
  1010. $next = end($links);
  1011. }
  1012. // Determine if we should exit the loop and $request = return.
  1013. if (!$next || $next['depth'] < $depth) {
  1014. break;
  1015. }
  1016. }
  1017. return $tree;
  1018. }
  1019. /**
  1020. * {@inheritdoc}
  1021. */
  1022. public function bookSubtreeData($link) {
  1023. $tree = &drupal_static(__METHOD__, []);
  1024. // Generate a cache ID (cid) specific for this $link.
  1025. $cid = 'book-links:subtree-cid:' . $link['nid'];
  1026. if (!isset($tree[$cid])) {
  1027. $tree_cid_cache = \Drupal::cache('data')->get($cid);
  1028. if ($tree_cid_cache && $tree_cid_cache->data) {
  1029. // If the cache entry exists, it will just be the cid for the actual
  1030. // data. This avoids duplication of large amounts of data.
  1031. $cache = \Drupal::cache('data')->get($tree_cid_cache->data);
  1032. if ($cache && isset($cache->data)) {
  1033. $data = $cache->data;
  1034. }
  1035. }
  1036. // If the subtree data was not in the cache, $data will be NULL.
  1037. if (!isset($data)) {
  1038. $result = $this->bookOutlineStorage->getBookSubtree($link, static::BOOK_MAX_DEPTH);
  1039. $links = [];
  1040. foreach ($result as $item) {
  1041. $links[] = $item;
  1042. }
  1043. $data['tree'] = $this->buildBookOutlineData($links, [], $link['depth']);
  1044. $data['node_links'] = [];
  1045. $this->bookTreeCollectNodeLinks($data['tree'], $data['node_links']);
  1046. // Compute the real cid for book subtree data.
  1047. $tree_cid = 'book-links:subtree-data:' . hash('sha256', serialize($data));
  1048. // Cache the data, if it is not already in the cache.
  1049. if (!\Drupal::cache('data')->get($tree_cid)) {
  1050. \Drupal::cache('data')->set($tree_cid, $data, Cache::PERMANENT, ['bid:' . $link['bid']]);
  1051. }
  1052. // Cache the cid of the (shared) data using the book and item-specific
  1053. // cid.
  1054. \Drupal::cache('data')->set($cid, $tree_cid, Cache::PERMANENT, ['bid:' . $link['bid']]);
  1055. }
  1056. // Check access for the current user to each item in the tree.
  1057. $this->bookTreeCheckAccess($data['tree'], $data['node_links']);
  1058. $tree[$cid] = $data['tree'];
  1059. }
  1060. return $tree[$cid];
  1061. }
  1062. }