Newer
Older
<?php
/**
* @file
* Contains \Drupal\book\BookManager.
*/
namespace Drupal\book;
use Drupal\Core\Database\Connection;
Angie Byron
committed
use Drupal\Core\Entity\EntityInterface;
Angie Byron
committed
use Drupal\Core\Entity\EntityManagerInterface;
Angie Byron
committed
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Config\ConfigFactory;
use Drupal\node\NodeInterface;
/**
* Book Manager Service.
*/
class BookManager {
/**
* Database Service Object.
*
* @var \Drupal\Core\Database\Connection
*/
Angie Byron
committed
protected $connection;
/**
* Entity manager Service Object.
*
Angie Byron
committed
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
Angie Byron
committed
/**
* The translation service.
*
* @var \Drupal\Core\StringTranslation\TranslationInterface
*/
protected $translation;
/**
* Config Factory Service Object.
*
* @var \Drupal\Core\Config\ConfigFactory
*/
protected $configFactory;
/**
* Books Array.
*
* @var array
*/
protected $books;
/**
* Constructs a BookManager object.
*/
Angie Byron
committed
public function __construct(Connection $connection, EntityManagerInterface $entity_manager, TranslationInterface $translation, ConfigFactory $config_factory) {
Angie Byron
committed
$this->connection = $connection;
$this->entityManager = $entity_manager;
$this->translation = $translation;
$this->configFactory = $config_factory;
}
/**
* Returns an array of all books.
*
* 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.
*/
public function getAllBooks() {
if (!isset($this->books)) {
$this->loadBooks();
}
return $this->books;
}
/**
* Loads Books Array.
*/
protected function loadBooks() {
$this->books = array();
Angie Byron
committed
$nids = $this->connection->query("SELECT DISTINCT(bid) FROM {book}")->fetchCol();
if ($nids) {
Angie Byron
committed
$query = $this->connection->select('book', 'b', array('fetch' => \PDO::FETCH_ASSOC));
$query->join('menu_links', 'ml', 'b.mlid = ml.mlid');
$query->fields('b');
$query->fields('ml');
$query->condition('b.nid', $nids);
$query->orderBy('ml.weight');
$query->orderBy('ml.link_title');
$query->addTag('node_access');
$query->addMetaData('base_table', 'book');
$book_links = $query->execute();
Dries Buytaert
committed
$nodes = $this->entityManager->getStorageController('node')->loadMultiple($nids);
foreach ($book_links as $link) {
$nid = $link['nid'];
if (isset($nodes[$nid]) && $nodes[$nid]->status) {
$link['href'] = $link['link_path'];
$link['options'] = unserialize($link['options']);
$link['title'] = $nodes[$nid]->label();
$link['type'] = $nodes[$nid]->bundle();
$this->books[$link['bid']] = $link;
}
}
}
}
Angie Byron
committed
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
/**
* Returns an array with default values for a book page's menu link.
*
* @param string|int $nid
* The ID of the node whose menu link is being created.
*
* @return array
* The default values for the menu link.
*/
public function getLinkDefaults($nid) {
return array(
'original_bid' => 0,
'menu_name' => '',
'nid' => $nid,
'bid' => 0,
'router_path' => 'node/%',
'plid' => 0,
'mlid' => 0,
'has_children' => 0,
'weight' => 0,
'module' => 'book',
'options' => array(),
);
}
/**
* Finds the depth limit for items in the parent select.
*
* @param array $book_link
* A fully loaded menu link that is part of the book hierarchy.
*
* @return int
* The depth limit for items in the parent select.
*/
public function getParentDepthLimit(array $book_link) {
return MENU_MAX_DEPTH - 1 - (($book_link['mlid'] && $book_link['has_children']) ? $this->entityManager->getStorageController('menu_link')->findChildrenRelativeDepth($book_link) : 0);
}
/**
* Builds the common elements of the book form for the node and outline forms.
*
* @param array $form
* An associative array containing the structure of the form.
* @param array $form_state
* An associative array containing the current state of the form.
* @param \Drupal\node\NodeInterface $node
* The node whose form is being viewed.
* @param \Drupal\Core\Session\AccountInterface $account
* The account viewing the form.
*
* @return array
* The form structure, with the book elements added.
*/
public function addFormElements(array $form, array &$form_state, NodeInterface $node, AccountInterface $account) {
// If the form is being processed during the Ajax callback of our book bid
// dropdown, then $form_state will hold the value that was selected.
if (isset($form_state['values']['book'])) {
$node->book = $form_state['values']['book'];
}
$form['book'] = array(
'#type' => 'details',
'#title' => $this->t('Book outline'),
'#weight' => 10,
'#collapsed' => TRUE,
'#group' => 'advanced',
'#attributes' => array(
'class' => array('book-outline-form'),
),
'#attached' => array(
'library' => array(array('book', 'drupal.book')),
),
'#tree' => TRUE,
);
foreach (array('menu_name', 'mlid', 'nid', 'router_path', 'has_children', 'options', 'module', 'original_bid', 'parent_depth_limit') as $key) {
$form['book'][$key] = array(
'#type' => 'value',
'#value' => $node->book[$key],
);
}
$form['book']['plid'] = $this->addParentSelectFormElements($node->book);
// @see \Drupal\book\Form\BookAdminEditForm::bookAdminTableTree(). The
// weight may be larger than 15.
Angie Byron
committed
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
$form['book']['weight'] = array(
'#type' => 'weight',
'#title' => $this->t('Weight'),
'#default_value' => $node->book['weight'],
'#delta' => max(15, abs($node->book['weight'])),
'#weight' => 5,
'#description' => $this->t('Pages at a given level are ordered first by weight and then by title.'),
);
$options = array();
$nid = !$node->isNew() ? $node->id() : 'new';
if ($node->id() && ($nid == $node->book['original_bid']) && ($node->book['parent_depth_limit'] == 0)) {
// This is the top level node in a maximum depth book and thus cannot be moved.
$options[$node->id()] = $node->label();
}
else {
foreach ($this->getAllBooks() as $book) {
$options[$book['nid']] = $book['title'];
}
}
if ($account->hasPermission('create new books') && ($nid == 'new' || ($nid != $node->book['original_bid']))) {
// The node can become a new book, if it is not one already.
$options = array($nid => $this->t('- Create a new book -')) + $options;
}
if (!$node->book['mlid']) {
// The node is not currently in the hierarchy.
$options = array(0 => $this->t('- None -')) + $options;
}
// Add a drop-down to select the destination book.
$form['book']['bid'] = array(
'#type' => 'select',
'#title' => $this->t('Book'),
'#default_value' => $node->book['bid'],
'#options' => $options,
'#access' => (bool) $options,
'#description' => $this->t('Your page will be a part of the selected book.'),
'#weight' => -5,
'#attributes' => array('class' => array('book-title-select')),
'#ajax' => array(
'callback' => 'book_form_update',
'wrapper' => 'edit-book-plid-wrapper',
'effect' => 'fade',
'speed' => 'fast',
),
);
return $form;
}
/**
* Determines if a node can be removed from the book.
*
* A node can be removed from a book if it is actually in a book and it either
* is not a top-level page or is a top-level page with no children.
*
* @param \Drupal\node\NodeInterface $node
* The node to remove from the outline.
*
* @return bool
* TRUE if a node can be removed from the book, FALSE otherwise.
*/
public function checkNodeIsRemovable(NodeInterface $node) {
return (!empty($node->book['bid']) && (($node->book['bid'] != $node->id()) || !$node->book['has_children']));
}
/**
* Handles additions and updates to the book outline.
*
* This common helper function performs all additions and updates to the book
* outline through node addition, node editing, node deletion, or the outline
* tab.
*
* @param \Drupal\node\NodeInterface $node
* The node that is being saved, added, deleted, or moved.
*
* @return bool
* TRUE if the menu link was saved; FALSE otherwise.
*/
public function updateOutline(NodeInterface $node) {
if (empty($node->book['bid'])) {
return FALSE;
}
$new = empty($node->book['mlid']);
$node->book['link_path'] = 'node/' . $node->id();
$node->book['link_title'] = $node->label();
$node->book['parent_mismatch'] = FALSE; // The normal case.
if ($node->book['bid'] == $node->id()) {
$node->book['plid'] = 0;
$node->book['menu_name'] = $this->createMenuName($node->id());
}
else {
// Check in case the parent is not is this book; the book takes precedence.
if (!empty($node->book['plid'])) {
$parent = $this->connection->query("SELECT * FROM {book} WHERE mlid = :mlid", array(
':mlid' => $node->book['plid'],
))->fetchAssoc();
}
if (empty($node->book['plid']) || !$parent || $parent['bid'] != $node->book['bid']) {
$node->book['plid'] = $this->connection->query("SELECT mlid FROM {book} WHERE nid = :nid", array(
':nid' => $node->book['bid'],
))->fetchField();
$node->book['parent_mismatch'] = TRUE; // Likely when JS is disabled.
}
}
$node->book = $this->entityManager
->getStorageController('menu_link')->create($node->book);
if ($node->book->save()) {
if ($new) {
// Insert new.
$this->connection->insert('book')
->fields(array(
'nid' => $node->id(),
'mlid' => $node->book['mlid'],
'bid' => $node->book['bid'],
))
->execute();
}
else {
if ($node->book['bid'] != $this->connection->query("SELECT bid FROM {book} WHERE nid = :nid", array(
':nid' => $node->id(),
))->fetchField()) {
// Update the bid for this page and all children.
catch
committed
$this->updateId($node->book);
Angie Byron
committed
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
}
}
return TRUE;
}
// Failed to save the menu link.
return FALSE;
}
/**
* Translates a string to the current language or to a given language.
*
* See the t() documentation for details.
*/
protected function t($string, array $args = array(), array $options = array()) {
return $this->translation->translate($string, $args, $options);
}
/**
* Generates the corresponding menu name from a book ID.
*
* @param $id
* The book ID for which to make a menu name.
*
* @return
* The menu name.
*/
public function createMenuName($id) {
return 'book-toc-' . $id;
}
/**
* Updates the book ID of a page and its children when it moves to a new book.
*
* @param array $book_link
* A fully loaded menu link that is part of the book hierarchy.
*/
catch
committed
public function updateId($book_link) {
Angie Byron
committed
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
$query = $this->connection->select('menu_links');
$query->addField('menu_links', 'mlid');
for ($i = 1; $i <= MENU_MAX_DEPTH && $book_link["p$i"]; $i++) {
$query->condition("p$i", $book_link["p$i"]);
}
$mlids = $query->execute()->fetchCol();
if ($mlids) {
$this->connection->update('book')
->fields(array('bid' => $book_link['bid']))
->condition('mlid', $mlids, 'IN')
->execute();
}
}
/**
* Builds the parent selection form element for the node form or outline tab.
*
* This function is also called when generating a new set of options during the
* Ajax callback, so an array is returned that can be used to replace an
* existing form element.
*
* @param array $book_link
* A fully loaded menu link that is part of the book hierarchy.
*
* @return array
* A parent selection form element.
*/
protected function addParentSelectFormElements(array $book_link) {
if ($this->configFactory->get('menu.settings')->get('override_parent_selector')) {
return array();
}
// Offer a message or a drop-down to choose a different parent page.
$form = array(
'#type' => 'hidden',
'#value' => -1,
'#prefix' => '<div id="edit-book-plid-wrapper">',
'#suffix' => '</div>',
);
if ($book_link['nid'] === $book_link['bid']) {
// This is a book - at the top level.
if ($book_link['original_bid'] === $book_link['bid']) {
$form['#prefix'] .= '<em>' . $this->t('This is the top-level page in this book.') . '</em>';
}
else {
$form['#prefix'] .= '<em>' . $this->t('This will be the top-level page in this book.') . '</em>';
}
}
elseif (!$book_link['bid']) {
$form['#prefix'] .= '<em>' . $this->t('No book selected.') . '</em>';
}
else {
$form = array(
'#type' => 'select',
'#title' => $this->t('Parent item'),
'#default_value' => $book_link['plid'],
'#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.', array('!maxdepth' => MENU_MAX_DEPTH)),
'#options' => $this->getTableOfContents($book_link['bid'], $book_link['parent_depth_limit'], array($book_link['mlid'])),
'#attributes' => array('class' => array('book-title-select')),
'#prefix' => '<div id="edit-book-plid-wrapper">',
'#suffix' => '</div>',
);
}
return $form;
}
/**
* Recursively processes and formats menu items 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
* 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.
* @param string $indent
* A string appended to each menu item 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).
* @param int $depth_limit
* Any link deeper than this value will be excluded (along with its children).
*/
protected function recurseTableOfContents(array $tree, $indent, array &$toc, array $exclude, $depth_limit) {
foreach ($tree as $data) {
if ($data['link']['depth'] > $depth_limit) {
// Don't iterate through any links on this level.
break;
}
if (!in_array($data['link']['mlid'], $exclude)) {
$toc[$data['link']['mlid']] = $indent . ' ' . truncate_utf8($data['link']['title'], 30, TRUE, TRUE);
if ($data['below']) {
$this->recurseTableOfContents($data['below'], $indent . '--', $toc, $exclude, $depth_limit);
}
}
}
}
/**
* Returns an array of book pages in table of contents order.
*
* @param int $bid
* The ID of the book whose pages are to be listed.
* @param int $depth_limit
* Any link deeper than this value will be excluded (along with its children).
* @param array $exclude
* (optional) An array of menu link ID values. Any link whose menu link ID is
* in this array will be excluded (along with its children). Defaults to an
* empty array.
*
* @return array
* An array of (menu link ID, title) pairs for use as options for selecting a
* book page.
*/
public function getTableOfContents($bid, $depth_limit, array $exclude = array()) {
$tree = menu_tree_all_data($this->createMenuName($bid));
$toc = array();
$this->recurseTableOfContents($tree, '', $toc, $exclude, $depth_limit);
return $toc;
}