Skip to main content

Blocks are a great way to add pieces or chunks of content to your Drupal site.  Capable of displaying simple text, images, forms or complex logic.  There are plenty of sites around that show you how to add a simple custom block.  However, if you are a developer like me, when you are writing your custom block programmatically it has far more requirements than a simple block.  In this article, I'll show how to:

  • Create your initial custom block;
  • Adding a database query programmatically to your block;
  • If you are taxonomy to assist in managing your content - how to do this programmatically;
  • Add templates to your block - note I'm also using tailwind css in the templates

 

By way of example the home screen of this site is a custom module.  As I'm still writing this article, in time I'll add the code on both GitHib and Drupal for access.

 

Create a module

Custom modules are created in your Drupal directory path /modules/custom/ of your Drupal installation.  Using this directory path create a new directory titled the name you want to give your block.  I'm creating a new block called snippets.  So my directory path is /modules/custom/snippets/.  Using shell (Terminal OSX), in the /module/custom/ create your new custom block directory by mkdir command: 

mkdir {block_name}

Replace {block_name} with the your directory name.  For the ease of simplicity in this article I'll refer my custom block being snippets.  

mkdir snippets

Then open the new directory through the command

cd snippets

In Drupal 8/9, it is necessary to create an info.yml file that contains the metadata for every custom module.  You will need to create the snippets.info.yml file in this directory.  To generate this file, use the command:

touch snippets.info.yml

Add the core meta data in to this file using vim:

vi snippets.info.yml

and add the following:

name: Snippets
description: Create home page list of recent vocabulary terms and showing their respective articles
type: module
package: custom
version: 1.0
core: 8.x

dependencies:
  - drupal:block
  - drupal:node
  - drupal:path

 

Create a Block Class

We’ll create a class that will contain the logic of our block.  The location of this class will be  /modules/custom/snippets/src/Plugin/Block directory.  Remember to replace snippets with the name of your block directory name.

Go ahead and create the necessary directories to add the path src/Plugin/Block.

In the Block directory create a file called {block_name}Block.yml.  My file will be called SnippetsBlock.yml.  The class file should contain annotation as well. The annotation allows us to identify the block, also this class will contain a series of methods:

  • build
  • blockForm
  • blockSubmit
  • blockValidate
  • getCacheMaxAge
  • access

 

build()

build(): required method which is expected to return a render array defining the content you want your block to display.

/**
* {@inheritdoc}
*/
public function build()
{
    $config = $this->getConfiguration();
    $headline = $config['snippets_headline'];
    return [
        '#markup' => "<span>$headline</span>",
    ];
}

 

blockForm()

blockForm(): This method allows you to define a block configuration form using the Form API.  The configuration form is editable from admin -> structure -> block layout in the backend.

/**
 * {@inheritdoc}
 */
public function blockForm($form, FormStateInterface $form_state)
{
    $form = parent::blockForm($form, $form_state);
    $config = $this->getConfiguration();
    $current_year = date("Y");
    $headline = $this->t('Developer how-to resource @year', [
        '@year' => $current_year
    ]);
    $form['snippets_headline'] = [
        '#type' => 'textfield',
        '#title' => $this->t('Headline'),
        '#description' => $this->t('Write your headline text.'),
        '#default_value' => isset($config['snippets_headline']) ? $config['snippets_headline'] : $headline,
    ];
    $byline = $this->t('Codebales holds an ever growing number of problems with solutions that we have experienced in our day to day code writing
    $form['snippets_byline'] = [
        '#type' => 'textarea',
        '#title' => $this->t('Byline'),
        '#description' => $this->t('Write your byline text.'),
        '#default_value' => isset($config['snippets_byline']) ? $config['snippets_byline'] : $byline,
    ];
    return $form;
}

 

blockSubmit()

blockSubmit(): This method used to save a configuration, defined on the blockForm() method.

/**
   * {@inheritdoc}
   */
public function blockSubmit($form, FormStateInterface $form_state) {
    parent::blockSubmit($form, $form_state);
    $this->configuration['snippets_block_settings'] = $form_state->getValue('snippets_block_settings');
    $this->setConfigurationValue('snippets_headline', $form_state->getValue('snippets_headline'));
    $this->setConfigurationValue('snippets_byline', $form_state->getValue('snippets_byline'));

}

blockValidate()

blockValidate(): This method used to validate block configuration form.

/**
 * {@inheritdoc}
 */
public function blockValidate($form, FormStateInterface $form_state)
  {
    // headline validate a value exists
    if (empty($form_state->getValue('snippets_headline'))) {
       $form_state->setErrorByName('snippets_headline', t('This field is required'));
    }
    if (empty($form_state->getValue('snippets_byline'))) {
       $form_state->setErrorByName('snippets_byline', t('This field is required'));
    }
}

 

getCacheMaxAge()

getCacheMaxAge(): This method used if you want to change block cache max time.  A cache max-age is a positive integer, expressing a number of seconds.  Therefore, 100 means cacheable for 100 seconds.  Whereas, 0 means cacheable for zero seconds or not cacheable.  Otherwise, permanent means cacheable forever and is written as Cache::PERMANENT.  More details about Drupal cache is found on Drupal cache api documentation cache-max-age page.

/**
 * {@inheritdoc}
 * return 0 If you want to disable caching for this block.
 */
public function getCacheMaxAge()
{
    // return Cache::PERMANENT;
    return 0;
}

 

access()

access(): Defines a custom user access logic. It is expected to return an AccessResult object.

/**
 * {@inheritdoc}
 */
public function access(AccountInterface $account, $return_as_object = FALSE)
{
    return AccessResult::allowedIfHasPermission($account, 'access content');
}

Querying the database

Okay so now you have basic set-up in place and working.  However, you are most likely going to have a need to query the database.  How is this done?  The database queries are best set up in a new file.  So in the src/ directory create a new file named SnippetsQuery.php.  In this newly created file, we are going to add the functions to perform the query.

Add the following code to the SnippetsQuery.php file

<?php

namespace Drupal\snippets;

use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Config\ConfigManagerInterface;

use Drupal\node\NodeInterface;
use Drupal\node\Entity\Node;
use Psr\Log\LoggerInterface;
use Drupal\taxonomy\Entity\Term;

/**
 * Defines the queries for the snippets
 */
class SnippetsQuery {

  use StringTranslationTrait;

  /**
   * [__construct description]
   */
  public function __construct() {
    $this->storage_type = 'node';
  }

  /**
   * Get the nids array
   *
   * @param array $nids [description]
   */
  public function getNids() {
    return $this->nids;
  }

  /**
   * Set the nids array
   *
   * @param [Array] $nids [description]
   */
  public function setNids($nids) {
    $this->nids = $nids;
  }

  /**
   * Get the nodes value
   *
   * @param [Array] $nodes [description]
   */
  public function getNodes() {
    return $this->nodes;
  }

  /**
   * Get the query value
   *
   * @param [Array] $nodes [description]
   */
  public function getQuery() {
    return $this->query;
  }

  /**
   * Set the entity_type value
   *
   * @param [Str] $entity_type [description]
   */
  public function setEntityType($entity_type) {
    $this->entity_type = $entity_type;
  }

  /**
   * Set the query value
   *
   * @param [Array] $query [description]
   */
  public function setQuery($query) {
    $this->query = $query;
  }

  /**
   * initialise the query
   *
   */
  public function initialiseQuery() {
    // set the entity query for node
    $this->query = \Drupal::entityQuery($this->storage_type);
  }

}

In the code above, the key areas that I'll point out are the __construct where the storage_type is set to node.  Note, this is kept adjustable so a query can be made to other entity types such as user or node to name two.  The other functions are either set or get... except for initialiseQuery.  In this function, the database initialises the $this->query variable.  There are two other functions that also need to referenced:

  /**
   * Set the load_node_boolean value
   *
   * @param [Boolean] $load_node_switch [description]
   */
  public function setLoadNodeSwitch($load_node_switch = TRUE) {
    $this->load_node_switch = $load_node_switch;
  }

 

  /**
   * Execute the query and define the nodes as $this->nodes
   *
   */
  public function loadNodeEntity() {
    // limit the query to a type
    if (!is_null($this->entity_type)) {
      $this->query->condition('type', $this->entity_type);
    }
    $nids = $this->query->execute();

    // load the nodes for this query if there is a result
    if (count($nids) > 0) {
      $this->nids = $nids;
      // update the nodes array
      if ($this->load_node_switch) {
        // create the nodes list from the nids array
        $this->loadMultipleNodesFromNids();
        // clean the nodes and remove unnecessary fields
        $this->buildNodes();
      }
    }
  }

 

  /**
   * Load nodes.nids
   *
   * @return [type] [description]
   */
  public function loadMultipleNodesFromNids() {
    $storage = \Drupal::entityTypeManager()->getStorage($this->storage_type);
    // Load actual user data from database.
    $storage->resetCache();
    // load multiple array
    $this->nodes_list = $storage->loadMultiple($this->nids);
  }

 

SnippetsBlock - adding a database query programmatically to your block

In the SnippetsBlock.php file let's add the code so we can use the SnippetsQuery.php structure.  There are a few of steps required in the SnippetsBlock so we can call the query class.  At the top of the SnippetsBlock file add

First step
use Drupal\snippets\SnippetsQuery;

Just after the class has been called, add 

  /**
   * Snippets query.
   *
   * @var \Drupal\snippets\SnippetsQuery
   */
  protected $SnippetsQuery;

 

Second step

Now add a new __construct function

  /**
   * Constructs an SnippetsBlock object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The snippets query base.
   * @param \Drupal\snippets\SnippetsQuery $SnippetsQuery
   */
  public function __construct(array $configuration,
                              $plugin_id,
                              $plugin_definition,
                              SnippetsQuery $SnippetsQuery) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->entity_type = 'article';
    $this->nids = array();
    $this->nodes = array();
    $this->SnippetsQuery = $SnippetsQuery;
    $this->storage_type = 'node';
  }

 

Third step

Generate the create function.  If this function is not added and you will receive and error.  The key here is to add $container->get('SnippetsQuery') in this function.

 /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('SnippetsQuery')
    );
  }

 

The last step in this sequence is to create a services file.  In the Snippets directory at the same level as where the snippets.info.yml exists, create a new file called snippets.services.yml.  In this file, we will add the SnippetsQuery class so Drupal knows that it exists.  Add the following code in the services file:

services:
  SnippetsQuery:
    class: Drupal\snippets\SnippetsQuery
    arguments: []

 

Error messages

Oh yes, love those error messages!

If you receive the following error message:

Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: You have requested a non-existent service "SnippetsQuery". in Drupal\Component\DependencyInjection\Container->get()(line 153 of /{path directory}/core/lib/Drupal/Component/DependencyInjection/Container.php).

What to do... check the following:

  • Have you created a {your_module}.services.yml file?
  • Have you added the line of code for the previous section and renamed it to your module?
  • services: 
      SnippetsQuery: 
        class: Drupal\snippets\SnippetsQuery 
    ​​​​​​​    arguments: []
  • In the {module}Block.php file have you:
    • Added the Query reference at the top of the file?  In my module block the code at the top is
<?php

namespace Drupal\snippets\Plugin\Block;

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Cache\Cache;
use Symfony\Component\HttpFoundation\RequestStack;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Url;
use Drupal\Core\Link;

use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
use Drupal\datetime\Plugin\Field\FieldFormatter\DateTimeCustomFormatter;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\datetime_range\DateTimeRangeTrait;

use Drupal\snippets\SnippetsQuery;
use Drupal\snippets\SnippetsPathAlias;
use Drupal\snippets\SnippetsTaxonomyTerms;
use Symfony\Component\DependencyInjection\ContainerInterface;