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.

Enable Twig debug via Devel PHP

Components

Enabling Twig debug info via Devel PHP is not something I recommend you to do but in some rare cases, you might have to do it. Here's how to do it:

use Symfony\Component\Yaml\Yaml;

$yaml = Yaml::parseFile('sites/default/default.services.yml');
$yaml['parameters']['twig.config']['debug'] = TRUE;
$updated_yaml = Yaml::dump($yaml);
file_put_contents('sites/default/services.yml', $updated_yaml);

Nothing fancy here. Just loading the default services file, changing one variable to TRUE and then saving the content to services.yml file.

Template suggestions for Drupal 9 block types

Components

By default Drupal 9 doesn't have a template suggestion for block types. If you enable theme debug and inspect a block you'll see something like this:

Image

To fix this and add a suggestion for all block types you have to implement the hook_theme_suggestions_hook() in your *.theme file:

/**
 * Implementation of hook_theme_suggestions_hook().
 */
function MY_THEME_theme_suggestions_block_alter(&$suggestions, $variables) {
  if (isset($variables['elements']['content']['#block_content'])) {
    $bundle = $variables['elements']['content']['#block_content']->bundle();
    array_splice($suggestions, 1, 0, 'block__' . $bundle);
  }
}

If you take a look at theme debug info now you'll see something like this:

Image

As you can see we now have a theme suggestion for the Image block type. Just by implementing one hook, you can now create a different Twig template for each block type.