Artificial Intelligence

Search by Content Type in Drupal 8 - a custom module tutorial

At the time of this writing, the Search API module is still in alpha phase. A note from the module maintainers warn that it is unstable. This blog post serves as a way to learn how to write a simple Drupal 8 module to


Filed under:

This article is one of Metal Toad's Top 20 Drupal Tips. Enjoy!

Note: this code has been tested in Drupal 8.0 and 8.1

At the time of this writing, the Search API module is still in alpha phase. A note from the module maintainers warn that it is unstable. This blog post serves as a way to learn how to write a simple Drupal 8 module to modify Drupal 8's core search without using the Search API module.

Scenario: we have a Drupal 8 site that shows all content types in the search results. We want to exclude certain content types (also known as "node bundles") from the results list. 

Fun D8 module stuff we'll learn along the way:

  • How to use simple, custom configurations in our module
  • What TAG's are, and how they're awesome

Here's a link to the GitHub repository for this example:
https://github.com/mikedotexe/exclude_bundles

So first: the big picture. I'd like to exclude certain bundles, but I'd also like the ability to change them. (That is to say, I don't want to hardcode them.) So we're going to build a settings page for our Drupal 8 module that looks like this:

Settings page for Exclude Bundles Drupal 8 custom module

If you look at the breadcrumbs at the top, you'll see this is added under the Admin >> Configuration >> Search and metadata. So we're also going to explore doing some routing, and making a menu link in an existing menu. That will give us the ability to place our settings page in the most logical place: right next to all the other Search Stuff.

/admin/config

Exclude bundles link shown in the /admin/config page

The checkboxes on our settings page will also be exportable using Drupal 8's Configuration Manager.
(drush cex is the configuration export command)

Using terminal to export Drupal 8 custom configurations

 

So, without further adieu, let's start looking at the code.


exclude_bundles.info.yml

Like Drupal 7's .info file, every custom Drupal 8 module needs an info YAML file.

name: Exclude bundles
type: module
description: Allows Drupal core search to exclude node bundles
core: 8.x
package: Custom
configure: exclude_bundles.admin.config

The only line that's any fun here is the configure key. That gives us the neat little gear icon on /admin/modules

Configure gear icon appearing on /admin/modules

But what is the deal with exclude_bundles.admin.config? Shouldn't it be /admin/config/search/bundles or some other path? Not in Drupal 8, and this is where routing comes into play.


exclude_bundles.routing.yml

exclude_bundles.admin.config:
  path: '/admin/config/search/bundles'
  defaults:
    _form: '\Drupal\exclude_bundles\Form\SearchBundles'
    _title: 'Bundles to exclude from search'
  requirements:
    _permission: 'administer search'

In Drupal 7, this used to be accomplished through hook_menu. Here's a good resource on the differences betweeen Drupal 7 and Drupal 8's routing.

In the above code, the line I'd like to highlight is the _form key under defaults. As you can see, this is pointing to a class defined by our module, exclude_bundles, which brings us to the next file we'll look at. (Note that we omit the ".php" when referencing this class.)


src/Form/SearchBundles.php

...
public function buildForm(array $form, FormStateInterface $form_state) {
    $config = $this->config('exclude_bundles.settings');
    $configured_bundles = $config->get('bundles');
    // get list of current Content Types
    $node_bundles = \Drupal::service('entity.manager')->getStorage('node_type')->loadMultiple();
...
public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->config('exclude_bundles.settings')
      ->set('bundles', $form_state->getValue('bundles'))
      ->save();
...

I've omitted sections of that file for readability purposes, but feel free to reference the full code here

The code above shows us putting Drupal 8's configuration manager into action. We're basically saying,

"Hey Drupal, I want to use configuration. Which one? Um.. let's call it exclude_bundles.settings. Let's read from that configuration and save to it as well."

~ fledgling Drupal 8 dev

Let's stop for a second and talk about Configuration Manager in Drupal 8. This is an amazing leap for Drupal, and takes the place of the very popular Features module that allows us to export configuration (Views, taxonomy vocabs, Panel variants, content types...) into code.

Say you have a team using source control, and you're tasked with making a new content type. You create the content type and fields (on your local machine) and export those changes. New config files will be created in your designated configuration directory, and you can push those up to your repository. A second developer can then pull down those changes, use configuration import, and their site will have the new content type. This blog isn't about Configuration Manager, so I'll move on from that topic, but want to set that foundation.

Why are we using configuration? Because our settings page contains checkboxes, allowing users to customize which content types should be excluded from search. These settings are placed into a configuration instead of a plain variable, meaning they can be exported/imported as code. It also means we can set defaults in code. If we wanted to ship our module with the default of "exclude Articles from search, but show Basic Page content in search results" you would add this file:


config/install/exclude_bundles.settings.yml

base_route_name: exclude_bundles.admin.config
bundles:
  article: article
  page: 0

Time for the .module file, which is the most fun.


exclude_bundles.module

use Drupal\Core\Database\Query\AlterableInterface;
 
/**
 * Implements hook_query_TAG_alter
 */
function exclude_bundles_query_search_node_search_alter(AlterableInterface $query) {
  // see the config file exclude_bundles.settings.yml
  $config = \Drupal::config('exclude_bundles.settings');
  $bundles_config = $config->get('bundles');
  $hidden_bundles = [];
  foreach ($bundles_config as $k => $v) {
    // if the value is not 0, add it to the array
    if ($v) {
      $hidden_bundles[] = $k;
    }
  }
 
  if (!empty($hidden_bundles)) {
    // add the extra table to join based on config logic
    $query->join('node', 'n2', 'n.nid = n2.nid');
    $query->condition('n2.type', $hidden_bundles, 'NOT IN');
  }
}

I read a great blog post by Phase 2 regarding Drupal core search, and went a step further. The code above implements hook_query_TAG_alter, which I had never used before. In fact the word TAG had me interested.

TAG is very similar to FORM_ID in hook_form_FORM_ID_alter

Sure, you can use the generic hook_form_alter to access any form, but why not drill down into the exact form we're intending to modify? The same idea applies to hook_query_alter.

Where is this tag being set? I'm glad you asked, 'cause I wondered the same thing.

Let's look into Drupal core:
core/modules/search/src/SearchQuery.php

Debugging searchExpression function in Drupal core's search module

 

Hence, our TAG is search_node_search, as shown in the module code above.

If we debug the tables that are included in the $query object, we'll see that there is an alias "n" for the table node_field_data.

Debugging of the tables from the query object

In the last if statement, we are simply joining another table (the node table) so that we can filter by content type. Donezo.

Lastly, let's place a menu link for this. 


exclude_bundles.links.menu.yml

exclude_bundles.admin.config:
  title: 'Exclude bundles'
  parent: system.admin_config_search
  description: 'Exclude content types from search.'
  route_name: exclude_bundles.admin.config
  menu_name: admin

To place this under the "Search and metadata" menu item, once again we'll use the route instead of the path on the line pointing to the parent. So system.admin_config_search is simply a route defined in the Drupal core's system module. All starting to make sense, right? Right!

And that's it! We've created a custom Drupal 8.0/8.1 module that alters Drupal core's search to filter by content type, and it uses Drupal 8's configuration manager, routing, and a menu link. I hope this demystifies some of the new module development processes for Drupal 8.

Similar posts

Get notified on new marketing insights

Be the first to know about new B2B SaaS Marketing insights to build or refine your marketing function with the tools and knowledge of today’s industry.