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.

Using Entity Type Manager to get a list of fields

Components

Get all entity reference fields that target media entities

To get a list of all entity reference fields that target media entities you can use the Entity type manager and the loadByProperties() method.

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

Get all dynamic entity reference fields

In the same way, you can get a list of dynamic entity reference fields.

$fields = \Drupal::entityTypeManager()->getStorage('field_storage_config')->loadByProperties([
  'type' => 'dynamic_entity_reference',
]);

The $fields variable will contain an array of FieldStorageConfig instances for each field that satisfies the condition.

Image

This won't return fields that are defined as BaseFieldDefinition in your code.

How to zip files in Drupal

Components

Creating a zip archive in Drupal is easy. First, make sure that your PHP is compiled with ZIP support, then prepare the directory where you want to create a zip file, and finally create the zip file.

In the following example, I'm creating a zip archive by adding the file uploaded in the field_file field.

use Drupal\node\NodeInterface;
use Drupal\Core\File\FileSystemInterface;

function MY_MODULE_create_zip_file(NodeInterface $node) {
  // Check if ZipArchive class exists.
  if (!class_exists('ZipArchive')) {
    \Drupal::logger('MY_MODULE')->warning('Could not compress file, PHP is compiled without zip support.');
  }

  // Prepare destination directory.
  $directory = 'public://node-zip-files/' . $node->bundle();
  $file_system = \Drupal::service('file_system');
  $file_system->prepareDirectory($directory, FileSystemInterface:: CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);

  // Open zip archive.
  $zip = new \ZipArchive();
  $zip_filename = $file_system->realpath($directory . '/' . $node->id() . '.zip');
  $result = $zip->open($zip_filename, constant('ZipArchive::CREATE'));
  if (!$result) {
    \Drupal::logger('MY_MODULE')->warning('Zip archive could not be created. Error: ' . $result);
  }

  // Add file uploaded to the provided node to the zip acrhive.
  $file = $node->get('field_file')->entity;
  $filepath = $file_system->realpath($file->getFileUri());
  $result = $zip->addFile($filepath, basename($file->getFileUri()));
  if (!$result) {
    \Drupal::logger('MY_MODULE')->warning('File could not be added to zip archive.');
  }

  // Close zip archive.
  $result = $zip->close();
  if (!$result) {
    \Drupal::logger('MY_MODULE')->warning('Zip archive could not be closed.');
  }
}

This function can be used like this:

$node = \Drupal::entityTypeManager()->getStorage('node')->load(SOME_NODE_ID);
MY_MODULE_create_zip_file($node);

The zip file will be created in the following directory: public://node-zip-files/NODE_BUNDLE/SOME_NODE_ID.zip

How to alter a route - Drupal 9

Components

Sometimes you have to alter a route that is defined by a contrib module or Drupal core. Don't hack the module and change the *.routing.yml file. That is a very bad idea because the next time you update the module those changes will be gone.

The best way to alter a route in Drupal 9 is to use a Route subscriber. In the following example, I had to change the allowed auth mechanism for the JWT auth issuer route.

MY_MODULE.services.yml

services:
  MY_MODULE.route_subscriber:
    class: Drupal\MY_MODULE\Routing\RouteSubscriber
    tags:
      - { name: event_subscriber }

 src/Routing/RouteSubscriber.php

<?php

namespace Drupal\MY_MODULE\Routing;

use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;

class RouteSubscriber extends RouteSubscriberBase {

  /**
   * {@inheritdoc}
   */
  protected function alterRoutes(RouteCollection $collection) {
    if ($route = $collection->get('jwt_auth_issuer.jwt_auth_issuer_controller_generateToken')) {
      $route->setOption('_auth', ['basic_auth', 'email_basic_auth']);
    }
  }

}

Obviously, the $route object has a bunch of different useful methods like $route->setDefault() and $route->setRequirement(), so you can change anything you want.

Fix duplicate file names

Components

Drupal won't allow you to have two files with the same filename in a directory. Depending on how you put the file in the directory, Drupal will either rename it or replace the existing file. But it will allow you to have two or more files with the same filename inside different directories. To find and rename all such files you can use the following code snippet:

function MY_MODULE_fix_duplicate_filenames() {
  $connection = \Drupal::database();
  $query = $connection->query("SELECT filename FROM file_managed GROUP BY filename having count(*) >1;");
  $items = $query->fetchAll();

  /** @var \Drupal\file\FileStorageInterface $file_storage */
  $file_storage = \Drupal::entityTypeManager()->getStorage('file');
  /** @var \Drupal\file\FileRepositoryInterface $file_repository */
  $file_repository = \Drupal::service('file.repository');

  foreach ($items as $item) {
    $files = $file_storage->loadByProperties(['filename' => $item->filename]);

    /** @var \Drupal\file\FileInterface $file */
    foreach ($files as $file) {
      $pathinfo = pathinfo($file->getFilename());
      if (!empty($pathinfo['filename'])) {
        if (file_exists($file->getFileUri())) {
          $filename_suffix = time() . '-' . random_int(100000, 1000000);
          $new_destination = str_replace($pathinfo['filename'], $pathinfo['filename'] . '-' . $filename_suffix, $file->getFileUri());
          $file_repository->move($file, $new_destination);
        }
      }
    }
  }
}

As you can see, this will append a suffix constructed from a timestamp and a random integer value.