How to see your Drupal View's query

Components

The other day I had an issue with a Drupal View. In some cases, the results weren't good, and I couldn't figure out why at first. My first thought was to check the query generated by this view, and that's how I quickly identified the problem.

To see the query of the View, I used the following code snippet:

use Drupal\views\ViewExecutable;

/**
 * Implements hook_views_post_execute().
 */
function MY_MODULE_views_post_execute(ViewExecutable $view) {
  $channel = 'view_query:' . $view->id();
  $message = $view->query->query();
  \Drupal::logger($channel)->debug($message);
}

My View was connected to the Search API, so the query wasn't a standard SQL query, but this method of logging the View's query will work for both regular Views and those connected to the Search API.

Add 3rd party setting to Node type in Drupal

Components

Let's say you want to add some custom settings, such as a checkbox to the Node type form, so you can later dynamically limit the query to only include node types that have this value checked.

Image

You can easily do this by using third-party settings. Alter the Node type form by adding a field to it, and then add the corresponding entity builder function:

use Drupal\Core\Form\FormStateInterface;
use Drupal\node\NodeTypeInterface;

/**
 * Implements hook_form_FORM_ID_alter().
 */
function MY_MODULE_form_node_type_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  /** @var \Drupal\node\Entity\NodeType $node_type */
  $node_type = $form_state->getFormObject()->getEntity();
  $custom_checkbox = $node_type->getThirdPartySetting('MY_MODULE', 'custom_checkbox', FALSE);

  $form['custom_checkbox'] = [
    '#type' => 'checkbox',
    '#title' => t('Custom checkbox'),
    '#default_value' => $custom_checkbox,
  ];

  $form['#entity_builders'][] = 'MY_MODULE_form_node_type_edit_form_builder';
}

/**
 * Entity builder for the node type form with custom options.
 */
function MY_MODULE_form_node_type_edit_form_builder($entity_type, NodeTypeInterface $type, &$form, FormStateInterface $form_state) {
  $type->setThirdPartySetting('MY_MODULE', 'custom_checkbox', $form_state->getValue('custom_checkbox'));
}

That's pretty much it, the only thing left to add is the config/schema file. This file should be named after your module, so in our example, it would be:

config/schema/MY_MODULE.schema.yml

and its content would be:

node.type.*.third_party.MY_MODULE:
  type: mapping
  label: 'Additional Note type settings'
  mapping:
    custom_checkbox:
      type: boolean
      label: 'Custom checkbox'

Now, later when we want to select only the content types that have this field checked, we could write a query like this:

function MY_MODULE_get_custom_checkbox_content_types(): array {
  $content_types = \Drupal::entityTypeManager()->getStorage('node_type')->loadMultiple();
  $custom_checkbox_content_types = [];

  foreach ($content_types as $content_type) {
    $custom_checkbox = $content_type->getThirdPartySetting('MY_MODULE', 'custom_checkbox', FALSE);
    if ($custom_checkbox) {
      $custom_checkbox_content_types[] = $content_type->id();
    }
  }

  return $custom_checkbox_content_types;
}

And that's it, a very useful feature that isn't difficult to implement and can help avoid hardcoding in a function where a condition related to the content type is checked.

How to modify Log In or Log Out menu links in Drupal

Components

Log In and Log Out menu links, which are located in the User account menu, are provided by the User module found in the Drupal core. You cannot change these labels through the interface, but only through code.

You can do this by altering the class set for the user.logout menu link:

/**
 * Implements hook_menu_links_discovered_alter().
 */
function MY_MODULE_menu_links_discovered_alter(&$links) {
  if (isset($links['user.logout']['class'])) {
    $links['user.logout']['class'] = 'Drupal\MY_MODULE\Plugin\Menu\MyLoginLogoutMenuLink';
  }
}

and then it is necessary to extend the appropriate class and override the getTitle() method:

<?php

namespace Drupal\MY_MODULE\Plugin\Menu;

use Drupal\user\Plugin\Menu\LoginLogoutMenuLink;

/**
 * Defines the MyLoginLogoutMenuLink class.
 */
class MyLoginLogoutMenuLink extends LoginLogoutMenuLink {

  /**
   * {@inheritdoc}
   */
  public function getTitle() {
    if ($this->currentUser->isAuthenticated()) {
      return $this->t('My Log out');
    }
    else {
      return $this->t('My Log in');
    }
  }

}

In this way, you can modify any other link that is provided by any contrib or core module.

If the title of the link is provided directly as a string and not through a class, then it is sufficient to just change the title in the hook:

/**
 * Implements hook_menu_links_discovered_alter().
 */
function MY_MODULE_menu_links_discovered_alter(&$links) {
  if (isset($links['user.page']['title'])) {
    $links['user.page']['title'] = 'My profile';
  }
}

The above example will change the title of the user.page link from My account to My Profile.

Drupal 10.2 and Drush

Components

If you encounter any of these errors and you are using Drupal 10.2:

[preflight] Class "Drush\Commands\PolicyCommands" does not exist
[warning] Drush command terminated abnormally.

or

PHP Fatal error:  Uncaught Error: Class "Commands\DrushCommands" not found in /drush/Commands/PolicyCommands.php:10

then you need to modify your PolicyCommands file because it probably does not have the correct namespace. This file is usually located at: drush/Commands/PolicyCommands.php

Check if the class has a namespace and if it is correct. The namespace should be: 

namespace Drush\Commands;

so the entire file, with a single policy that forbids overwriting non-local databases, might look like this:

<?php

namespace Drush\Commands;

use Consolidation\AnnotatedCommand\CommandData;
use Consolidation\AnnotatedCommand\Hooks\HookManager;
use Drush\Attributes as CLI;
use Drush\Commands\sql\SqlSyncCommands;

class PolicyCommands extends DrushCommands {

  /**
   * Prevent catastrophic braino. Note that this file has to be local to the
   * machine that initiates the sql:sync command.
   *
   * @throws \Exception
   */
  #[CLI\Hook(type: HookManager::ARGUMENT_VALIDATOR, target: SqlSyncCommands::SYNC)]
  public function sqlSyncValidate(CommandData $commandData)
  {
    if ($commandData->input()->getArgument('target') !== '@self') {
      throw new \Exception(dt('Per !file, you may never overwrite the non-local database.', ['!file' => __FILE__]));
    }
  }

}

Although the official Drush documentation shows that the argument is called destination, it is actually called target.

Another thing I would like to draw attention to is if you see this error:

In ProcessBase.php line 171:
                
  Unable to decode output into JSON: Syntax error                                                                                                                                               
  TypeError: ArrayObject::__construct(): Argument #1 ($array) must be of type array, bool given in ArrayObject->__construct() (line 15 of vendor/consolidation/output-formatters/src/StructuredData/AbstractListData.php)

This probably means that you are using an old version of Drush in combination with Drupal 10.2. The solution is to install at least Drush version 12.4.3.

Please note that the Drush launcher has been archived and does not work with Drush 12. You should use one of the alternative solutions.

Creating a custom filter in Drupal Views for yearly filtering

Components

This snippet provides a custom Views filter for Drupal, enabling users to filter nodes by the year of their publication date. It adds a user-friendly dropdown in the Views UI, allowing easy selection of a year to filter content.

The hook_views_data() hook in Drupal is used to define how a module exposes database tables to Views.

/**
 * Implements hook_views_data().
 */
function MY_MODULE_views_data() {
  $data = [];

  $data['views']['year_filter'] = [
    'title' => t('Year filter - Custom Filter'),
    'filter' => [
      'title' => t('Year filter - Custom Filter'),
      'field' => 'created',
      'id' => 'year_filter',
    ],
  ];

  return $data;
}

To correctly implement your custom filter plugin in Drupal, you should place the code in the MY_MODULE/src/Plugin/views/filter directory. Below is an example of how your plugin code might look:

<?php

namespace Drupal\MY_MODULE\Plugin\views\filter;

use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\filter\FilterPluginBase;

/**
 * Filters nodes by year of publish date.
 *
 * @ViewsFilter("year_filter")
 */
class YearFilter extends FilterPluginBase {

  /**
   * {@inheritdoc}
   */
  public function adminSummary() {
    return $this->t('Filters nodes by year of publish date.');
  }

  /**
   * {@inheritdoc}
   */
  protected function valueForm(&$form, FormStateInterface $form_state) {
    $current_year = date('Y');
    $options = [];

    for ($year = 2023; $year <= $current_year; $year++) {
      $options[$year] = $year;
    }

    $form['value'] = [
      '#type' => 'select',
      '#title' => $this->t('Year'),
      '#options' => $options,
      '#default_value' => $current_year,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function query() {
    $this->ensureMyTable();

    /** @var \Drupal\views\Plugin\views\query\Sql $query */
    $query = $this->query;

    if (!empty($this->value[0])) {
      $query->addWhereExpression(0, "EXTRACT(YEAR FROM FROM_UNIXTIME(node_field_data.created)) = :year", [':year' => $this->value[0]]);
    }
  }

}

Clear the caches after implementation, and then add the custom filter to a View used to display nodes.

Creating a custom filter in Drupal Views for monthly filtering

Components

This snippet provides a custom Views filter for Drupal, enabling users to filter nodes by the month of their publication date. It adds a user-friendly dropdown in the Views UI, allowing easy selection of a month to filter content.

The hook_views_data() hook in Drupal is used to define how a module exposes database tables to Views.

/**
 * Implements hook_views_data().
 */
function MY_MODULE_views_data() {
  $data = [];

  $data['views']['month_filter'] = [
    'title' => t('Month filter - Custom Filter'),
    'filter' => [
      'title' => t('Month filter - Custom Filter'),
      'field' => 'created',
      'id' => 'month_filter',
    ],
  ];

  return $data;
}

To correctly implement your custom filter plugin in Drupal, you should place the code in the MY_MODULE/src/Plugin/views/filter directory. Below is an example of how your plugin code might look:

<?php

namespace Drupal\MY_MODULE\Plugin\views\filter;

use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\filter\FilterPluginBase;

/**
 * Filters nodes by month of publish date.
 *
 * @ViewsFilter("month_filter")
 */
class MonthFilter extends FilterPluginBase {

  /**
   * {@inheritdoc}
   */
  public function adminSummary() {
    return $this->t('Filters nodes by month of publish date.');
  }

  /**
   * {@inheritdoc}
   */
  protected function valueForm(&$form, FormStateInterface $form_state) {
    $form['value'] = [
      '#type' => 'select',
      '#title' => $this->t('Month'),
      '#options' => [
        'january' => $this->t('January'),
        'february' => $this->t('February'),
        'march' => $this->t('March'),
        'april' => $this->t('April'),
        'may' => $this->t('May'),
        'june' => $this->t('June'),
        'july' => $this->t('July'),
        'august' => $this->t('August'),
        'september' => $this->t('September'),
        'october' => $this->t('October'),
        'november' => $this->t('November'),
        'december' => $this->t('December'),
      ],
      '#default_value' => $this->value,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function query() {
    $this->ensureMyTable();

    /** @var \Drupal\views\Plugin\views\query\Sql $query */
    $query = $this->query;

    if (!empty($this->value[0])) {
      $month_num = date('m', strtotime($this->value[0]));
      $query->addWhereExpression(0, "EXTRACT(MONTH FROM FROM_UNIXTIME(node_field_data.created)) = :month", [':month' => $month_num]);
    }
  }

}

Clear the caches after implementation, and then add the custom filter to a View used to display nodes.

Passing data from PHP to JavaScript in Drupal

Components

This code demonstrates how to make data from a Drupal module available to JavaScript. This technique is often used when there's a need to interact with or manipulate the DOM using values or configurations from the backend.

Initially, we implement the hook_page_attachments() hook to attach data and assets to the page. Here, a cookie_name is retrieved and subsequently passed to JavaScript via the drupalSettings mechanism.

/**
 * Implements hook_page_attachments().
 */
function MY_MODULE_page_attachments(array &$attachments) {
  $cookie_name = some_func();
  $attachments['#attached']['drupalSettings']['MY_MODULE']['cookieName'] = $cookie_name;
  $attachments['#attached']['library'][] = 'MY_MODULE/MY_MODULE';
}

A library, MY_LIBRARY_NAME (found in the MY_MODULE.libraries.yml file), is defined, which references a JavaScript file (js/MY_MODULE.js) and lists its dependencies. This ensures that the script, along with the required core functionalities, gets loaded as needed.

MY_LIBRARY_NAME:
  js:
    js/MY_MODULE.js: {}
  dependencies:
    - core/drupal
    - core/once
    - core/drupalSettings

In the JavaScript portion (found in the js/MY_MODULE.js file), the data passed from PHP is accessed through the drupalSettings object within a Drupal behavior.

(function (Drupal, once, drupalSettings) {

  'use strict';

  Drupal.behaviors.myModuleBehavior = {
    attach: function (context, settings) {
      const elements = once('myModuleBehavior', 'html', context);
      
      elements.forEach(function () {
        let cookieName = drupalSettings.MY_MODULE.cookieName;
        ...
        // do something with cookieName
      });
    }
  };

})(Drupal, once, drupalSettings);

With this setup, any time Drupal renders the page, the cookieName from the PHP backend is made accessible in the JavaScript front end, providing seamless integration between the two.

How to find where media entity is used?

Components

To find out where a media entity is used on your Drupal site, you can use the following query. Just replace the $media_id with your Media ID and execute the query.

$media_id = YOUR MEDIA ID;

// Get all media fields.
$media_fields = \Drupal::entityTypeManager()->getStorage('field_storage_config')->loadByProperties([
  'type' => 'entity_reference',
  'settings' => [
    'target_type' => 'media',
  ],
]);

// Get a query for each field table.
$queries = [];

foreach ($media_fields as $media_field) {
  $name = $media_field->getName();
  $entity_type = $media_field->get('entity_type');
  $table_name = "{$entity_type}__{$name}";
  $query = \Drupal::database()->select($table_name, 'T');
  $query->fields('T', ['entity_id', "{$name}_target_id"]);
  $query->where("{$name}_target_id = :id", [':id' => $media_id]);
  $result = $query->execute()->fetchAll();
  dsm($entity_type);
  dsm($result);
}

The easiest way to execute this query is probably in Devel PHP. 

Duplicate key value violates unique constraint "path_alias____pkey"

Components

If you get an error that looks like this while saving a node or a taxonomy term in Drupal, it may mean that the table that stores path aliases in the database has become corrupted for some reason.

Drupal\Core\Entity\EntityStorageException: SQLSTATE[23505]: Unique violation: 7 
ERROR: duplicate key value violates unique constraint "path_alias____pkey" DETAIL: 
Key (id)=(30) already exists.: INSERT INTO "path_alias" ("revision_id", "uuid", 
"langcode", "path", "alias", "status") VALUES (:db_insert_placeholder_0, 
:db_insert_placeholder_1, :db_insert_placeholder_2, :db_insert_placeholder_3, 
:db_insert_placeholder_4, :db_insert_placeholder_5) RETURNING id; Array ( ) in 
Drupal\Core\Entity\Sql\SqlContentEntityStorage->save() (line 815 of 
/home/www-data/packages/drupal/web/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php).

Since aliases can always be regenerated (if you are using the Pathauto module), you can safely delete them. So, to fix the error truncate both path alias tables:

$tables = [
  'path_alias',
  'path_alias_revision',
];

foreach ($tables as $table) {
  \Drupal::database()->truncate($table)->execute();
}

then proceed to the Bulk generate page (admin/config/search/path/update_bulk) and generate path aliases again.

Before doing this make sure that you don't have any manually entered ('Generate automatic URL alias' unchecked) path aliases.

Image

Warning: If you have manually entered path aliases then don't use this method to fix the error, because you will lose content that can't be regenerated automatically.

Get meta tags programmatically

Components

To access the meta tags field data created by the Metatag module you can use the Metatag Manager service.

$metatag_manager = \Drupal::service('metatag.manager');

First, get the tags from the entity (usually from a node):

$tags = $metatag_manager->tagsFromEntityWithDefaults($node);

and then generate the meta tag values:

$metatags = $metatag_manager->generateTokenValues($tags, $node);

In summary, the Metatag Manager service in Drupal allows for programmatic retrieval of meta tags associated with an entity.