Drupal 8: How to Swap Out a Core Service

Drupal 8: How to Swap Out a Core Service

Book page - 5 years 9 months ago

This article describes how to swap out a service in the BookManager Class which is found in the file core/modules/book/src/BookManager.php.   This class provides the services that manage the book navigation menus.

When editing the order and titles of a book menu, I want the program to also display unpublished pages.  My workflow when creating a new book is to brainstorm an outline by first creating a bunch of blank pages.  I use the book menu editing page to arrange them into the correct order.   By doing this I break the enormous task of writing a book into lots of individual pages.

The problem is that the core book module only displays published pages.  I found the function that queries the database and realized all I need to do is comment out one line which sets a query condition for page status.  But it's in core, and you don't hack core.  Thanks to the wonders of Drupal 8 and services, I can easily swap out that service - and after a little research I found it's really pretty easy.  

Below is the core function I want to swap out.  It's found in the BookManager class in file core/modules/book/src/BookManager.php.   I added a comment to the line I want to comment out.

  public function bookTreeCheckAccess(&$tree, $node_links = []) {
    if ($node_links) {
      // @todo Extract that into its own method.
      $nids = array_keys($node_links);

      // @todo This should be actually filtering on the desired node status
      //   field language and just fall back to the default language.
      $nids = \Drupal::entityQuery('node')
        ->condition('nid', $nids, 'IN')
        ->condition('status', 1)    // If I comment out this line I get unpublished pages.
        ->execute();

      foreach ($nids as $nid) {
        foreach ($node_links[$nid] as $mlid => $link) {
          $node_links[$nid][$mlid]['access'] = TRUE;
        }
      }
    }
    $this->doBookTreeCheckAccess($tree);
  }

For this example we will create a new module called az_book.  Create the directory modules/custom/az_book.  Then create the az_book.info.yml file as follows:

name: AZ Book
type: module
description: Provide custom functionality to Book Menus
core: 8.x
package: AZ

Next look at the core/modules/book/book.services.yml file.  Find the book.manager service and copy the lines into your az_book.services.yml file.  Be sure to change the service name to az_book.manager and set the correct path to your class file:

services:
  az_book.manager:
    class: Drupal\az_book\AzBookManager
    arguments: ['@entity.manager', '@string_translation', '@config.factory', '@book.outline_storage', '@renderer']

Now we create a service manager class which tells Drupal we are extending the core BookManager services with our own.  Create the file modules/custom/az_book/src/AzServiceManager.php as follows:

<?php

namespace Drupal\az_book;

use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
  
/** 
 * Defines a book manager which extends the core BookManager class.
 */ 
class AzBookServiceProvider extends ServiceProviderBase {
  
  /**
   * {@inheritdoc}
   */
  public function alter(ContainerBuilder $container) {
    $definition = $container->getDefinition('book.manager');
    $definition->setClass('Drupal\az_book\AzBookManager');
  }
}

Finally create your new AzBookManager class as below.  This class only needs the one function you are swapping out.  It's identical to the core BookManager::bookTreeCheckAccess function except I've broken the query into separate statements so I can add an if statement to filter out unpublished pages only if this isn't the book menu edit page (route: book.admin_edit).

​​<?php

namespace Drupal\az_book;

use Drupal\book\BookManager;

// Override BookManager service.
//   - Include unpublished books in book outline edit form.

class AzBookManager extends BookManager {
 
  /**
   * {@inheritdoc}
   *
   * Override core and Load books that are not published.
   */
  public function bookTreeCheckAccess(&$tree, $node_links = []) {
    if ($node_links) {
      // @todo Extract that into its own method.
      $nids = array_keys($node_links);

      // @todo This should be actually filtering on the desired node status
      //   field language and just fall back to the default language.
      $query = \Drupal::entityQuery('node')
        ->condition('nid', $nids, 'IN');

      if (\Drupal::routeMatch()->getRouteName() != 'book.admin_edit') {
        $query->condition('status', 1);
      };

      $nids = $query->execute();

      foreach ($nids as $nid) {
        foreach ($node_links[$nid] as $mlid => $link) {
          $node_links[$nid][$mlid]['access'] = TRUE;
        }
      }
    }
    $this->doBookTreeCheckAccess($tree);
  }
}

Clear cache and you're done.

There are many more services in the BookManager class.   I didn't try it but I'm pretty sure you can swap out any of them using this method.