Module

Caching Data

Caching Data

Here's a simple way to cache data if the item itself has a lot of logic that bogs the server down...

Let's say you run a batch process and build a catalog item for an e-comm store.

Your 'foreach' would be converted into a batch process, but for sake of explaining caching here, we'll assume that a 'foreach' loop wouldn't timeout. :)

foreach ($products as $product) {
	// Again, do not use a foreach for large data sets.
	generateProductTile($product);
}

/**
 * Generates product tiles with a lot of logic behind how they are displayed
 */
function generateProductTile($product) {
	$drupalCache = \Drupal::cache();
	$cid = 'my_module:product_tile: . ' $product->id;
	// If we've already cached this result, we don't have to do any
	// excessive processing.
	if ($cached = $drupalCache->get($cid)) {
		if (!empty($cached)) {
			return $cached->data;
		}
	}
	
	// If we don't have a cache object, we do whatever we need to build the output.
	$title = $product->label();
	$price = $product->price();
	
	// Maybe we add some logic to the price, and this process is pretty intensive.
	$price = change_price($price);
	
	// Maybe we need to get an image... e-comm entities have weird ways of storing images
	// so this may also take a while.
	$image = get_product_image($product);
	
	// Let's pretend we're done... now we can render this tile.
	$themedTile = [
		'#theme' => 'product_tile',
		'#data' => [
			'title' => $title,
			'price' => $price,
			'image' => $image
		]
	];
	
	$rendererService = \Drupal::service('renderer');
	// This is just a basic way to render the basic html for this item
	// based on a hook_theme entry.
	$renderedTile = rendererService->renderRoot($themedTile);
	
	// Now that we did all that work, lets cache it so if we run this again
	// we don't have to do all that logic again. We can just gather pre-rendered tiles.
	// Setting the tag implies that when you save or update a product entity
	// this cache will be invalidated and rebuild itself the next time it's called.
	// (Double check the tag format for the entity)
	$drupalCache->set(cid: $cid, data: $renderedTile, tags: ['product:' . $product->id()]);
	
	// In the ->set method, you can set the expiration as well. By default it is 'permanent'
	// If you want some other caching policy, do that.
	
	// Since we want the built item, return it.
	return $renderedTile;
}

As an added bonus, here's the hook_theme  in the .module file, and the twig format to render this.

<?php

/**
 * Implements hook_theme().
 */
 function MY_MODULE_theme() {
 	return [
 		'product_tile' => [
 			'variables' => ['data' => NULL],
 			'template' => 'product_tile' /* This assumes the file is in the /templates directory within your module */
 		]
 	];
 }

And your twig file in MY_MODULE/templates directory.

** the image could also be pre-rendered, but this is more to show how you can use parts of variables as data within your twig file.

<h1>{{ data.title }}</h1>
<div class="price">{{ data.price }}</div>
<img src="{{ data.image.src }}" alt="{{ data.image.alt }}">

Get Image From Media Entity

Get Image From Media Entity

Using the media library is a great way to manage images (and other assets really) that may be used multiple times across your site, or even may need to be updated globally.

Programmatically referencing them can be a pain... Here's a simple function that can be used to grab from an image field in a media entity. This will give you the url and the alt properties so you can use them in a template file.

 

// Get the media ID from a field within your node or any other entity
if (!$entity->hasField('field_node_image')) {
	if (!$entity->get('field_node_image')->isEmpty()) {
		$mediaEntityId = $entity->get('field_node_image')->getValue()[0]['target_id'];
		$imageData = getMediaFieldImage($mediaEntityId);
	}
}

function getMediaFieldImage($mediaId) {
	// Use dependency injection as suggested.
    $entityTypeManager = \Drupal::entityTypeManager();
    $loggerFactory = \Drupal::logger('my_module');
    
    // Set a default... 
    $mediaImage = ['url' => '', 'alt' => ''];
    
    // Use a try catch because it fails a bit more gracefully.
    try {
      $media = $entityTypeManager->getStorage('media')->load($mediaId);
      // Get the field instances of type image
      $fieldDefinitions = $media->getFieldDefinitions();
    } catch (\Exception $e) {
      $loggerFactory->error('Error loading media: @error', ['@error' => $e->getMessage()]);
      return $mediaImage;
    }
    $imageField = FALSE;
    
    // This is really assuming that you have a single 'image' field on the media entity.
    // You could also get any other 'type' you want.
    foreach ($fieldDefinitions as $field) {
      if ($field->getType() == 'image') {
        $imageField = $field->getName();
        // If you want to do multiple images... well, change it up.
        break;
      }
    }
    if ($imageField) {
      // Now that we know the field to get data from, we get the field
      // data just like on any other entity.
      $image = $media->get($imageField)->getValue();
      if(!empty($image)) {
        try {
          $fid = $image[0]['target_id'];
          $file = $entityTypeManager->getStorage('file')->load($fid);
          if ($file) {
            $mediaImage['url'] = $file->createFileUrl();
            $mediaImage['alt'] = $image[0]['alt'];
          }
        } catch (\Exception $e) {
          $loggerFactory->error('Error loading file: @error', ['@error' => $e->getMessage()]);
        }
      }
    }
    return $mediaImage;

  }

 

Admin configuration form with dependency injection

Admin configuration form with dependency injection

When adding a form using Form API, best practice is to use dependency injection when possible. Admin form classes typically 'extend ConfigFormBase'. If you want to add to it, it'll look like this.

 

<?php

namespace Drupal\my_module\Form;

use Drupal\Core\Config\Config;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Class myCustomAdminForm.
 *
 * @package Drupal\my_module\Form
 */
class myCustomAdminForm extends ConfigFormBase {
  
  /**
   * @var $configFactory
   */
  protected $configFactory;
  
  /**
   * @var Config $myConfig
   */
  protected $myConfig;

  /**
   * The Messenger service.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $messenger;
  
  public function __construct(
    ConfigFactoryInterface $configFactory,
    MessengerInterface $messenger
  ) {
      parent::__construct($configFactory);
      $this->myConfig = $this->config('my_config.settings.my_settings');
      $this->messenger = $messenger;
  }
  
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('config.factory'),
      $container->get('messenger')
    );
  }
  
    /**
   * {@inheritdoc}
   */
  protected function getEditableConfigNames() {
    return [
      'my_config.settings.my_settings',
    ];
  }
  
  /**
   * This needs to return a string that is the unique ID of your form. Namespace the form ID based on your module's name.
   */
  public function getFormId() {
    return 'my_module_admin_form';
  }
  
  /**
   * This returns a Form API array that defines each of the elements your form is composed of.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
  
  	$form['text_input'] = [
  		'#type' => 'textfield',
  		'#description' => t('Enter data into this field'),
  		'#default_value' => $this->myConfig->get('my_config_values')
  	]
  	
  	return parent::buildForm($form, $form_state);
  
  }
	
  /**
   * Validate the form
   */  
  public function validateForm(array &$form, FormStateInterface $form_state) {
  	$values = $form_state->getValues();
  	if(empty($values['text_input']) {
  		$form_state->setErrorByName('text_input', t('You must enter data into this field.'));
  	}

  }
  
  /**
   * Submit the form
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
  	$values = $form_state->getValues();
  	
  	$this->myConfig->set('my_config_values', $values['text_input']);
  	
  	$this->myConfig->save();
  	
  	$this->messenger->addStatus('Your value has been saved.');
  	
  	parent::submitForm($form, $form_state);
  }

}

Output Twig Values in console.log

Output Twig Values in console.log

Trying to debug twig? Not rendering? Try this from within your twig file...

<script>console.log({{ _context | json_encode | raw}});</script>

 

Create a simple service

Create a simple service

Also includes dependency injection

MY_MODULE.services.yml

services:
  my_module.cool_service:
    class: Drupal\my_module\Services\CoolService
    arguments: ['@logger.factory']

/src/Services/CoolService.php

<?php

namespace Drupal\my_module\Services;

use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * CoolService returns what you give it in an array.
 */
class CoolService {


  /**
   * Logger Factory.
   *
   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
   */
  protected $loggerFactory;


  /**
   * Constructs the Cool service.
   *
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
   *   A Guzzle client object.
   */
  public function __construct(LoggerChannelFactoryInterface $loggerFactory) {
    $this->loggerFactory = $loggerFactory->get('my_module');
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('logger.factory')
    );
  }

  public function doSomething($someData) {
    $output = [
      'my_data' => $someData;
    ];

    $this->loggerFactory->error('Lets log a message!');
    
    return $output;
  }


}

Calling the service

$coolService = \Drupal::service('my_module.cool_service');

$returnedData = $coolService->doSomething('My Data');

echo $returnedData;

 

Sending E-Mail in Drupal 8

Sending E-Mail in Drupal 8

Need to send an email from your custom module?

First, let's make a template file so you can add some HTML markup if you want. This will go in your custom_module/templates directory.

<h1>Here is a header for your e-mail</h1>
{{ text|raw }}

Sincerely,
{{ sender_name|raw }}

Some pieces that will go into your custom module

Include the template in your hook_theme()

Be sure to clear your cache for this template to be used.

/**
 * Implements hook_theme().
 * To add the template definition.
 **/
function MY_MODULE_theme($existing, $type, $theme, $path) {
  return [
    'my-custom-email' => [
      'variables' => [
        'text' => NULL,
        'sender_name' => NULL,
      ],
    ], 
  ];  
}

Add hook_mail()

/**
* Implements hook_mail().
*/
function MY_MODULE_mail($key, &$message, $params) {
 $options = array(
   'langcode' => $message['langcode'],
 );
 switch ($key) {
   case 'my_custom_key':
      $message['headers'] = $params['headers'];
      $message['from'] = $params['from'];
      $message['subject'] = $params['subject'];
      $message['body'][] = $params['message'];
      $message['params']['files'] = $params['attachments'];
      break;
 }
}

Now, you can wrap the emailing into a function while using variables such as the recipient, sender, etc.

<?php
  
function _send_my_custom_email($data) {
  // Set a reply-to value
  $reply_to = $data['reply_to'];
  
  // Set a from value
  $from = $data['from'];
  
  // Set a subject
  $subject = $data['subject']
  
  // Set a sender name
  $sender_name = $data['sender_name'];
  
  // Set a recipient email
  $to = $data['to'];
  
  // Set your message body
  $body = $data['body'];
  
  // Use the theme to send the message
  $theme_body = array(
    '#theme' => 'my-custom-email',
    '#text' => $body,
    '#sender_name' => $sender_name,
  );
  
  // Set up the Drupal Mail manager
  $mailManager = \Drupal::service('plugin.manager.mail');
  
  // Tell the mail system what module you are sending this from
  $module = 'my_custom_module';
  
  // Set up the parameters for your email from the variables
  $params['message'] = drupal_render($theme_body);
  // Set some header information
  $params['headers'] = [
    'content-type' => 'text/html',
    'MIME-Version' => '1.0',
    'reply-to' => $data['from'],
    'from' => 'sender name <' . $from . '>',
    'Return-Path' => 'sender name <' . $from . '>',
  ];
  $params['from'] = $from;
  $params['subject'] = $subject;
  
  // Need to attach a file? Maybe uploaded through a form
  if(!empty($data['attach'])) {
    $file_info = array();
    $file_info['filepath'] = $data['attach']['uri']; // File path
    $file_info['filename'] = $data['attach']['filename']; //File name
    $file_info['filemime'] = $data['attach']['filemime']; //File mime type
    $params['attachments'][] = $file_info;
  }
  
  
  // Set a key so you can send different types of message
  $key = 'my_custom_key';
  
  // Get the language code from your site
  $langcode = \Drupal::currentUser()->getPreferredLangcode();
  
  // Set sending to true in case there is other logic that needs to happen before
  $send = true;
  
  // Send the E-Mail
  $result = $mailManager->mail($module, $key, $to, $langcode, $params, NULL, $send);

}

Now, you can call the function to email while passing along some parameters

// Within a function that you are using that requires an email to be sent, you call the function with some data

use Drupal\file\Entity\File;

function _something_should_be_happening() {
  if($something == 'happened') {
    $data['reply_to'] = 'reply_to@example.com';
    $data['from'] = 'from@example.com';
    $data['subject'] = 'Subject text here';
    $data['sender_name'] = 'John Doe'; 
    $data['to'] = 'receiver@example.com';
    $data['body'] = 'A long value that can also hold <div>HTML Markup</div>';
    
    if($add_a_file == TRUE) {
      // Need to send a file attachment?
      // Load a file
      $file = File::load('FILE_ID');
      // attach some file info
      $data['attach']['uri'] = $file->getFileUri();
      $data['attach']['filename'] = $file->getFileName();
      $data['attach']['filemime'] = $file->getMimeType();
    }
  
    _send_my_custom_email($data);
    return;
  } else {
    return;
  }
}

 

Creating select form using Taxonomy and Form API

Creating select form using Taxonomy and Form API

Often times when using the Drupal Form API creating custom forms, you'll need to get a select list of Taxonomy terms as options. These snippets will help you look up terms in a vocabulary and put them in your options array.

Drupal 8

// Create empty options array
$options = array();

// Taxonomy vocabulary machine name
$taxonomy = 'tax_machine_name';

// Get all the taxonomy terms from the vocabulary
$tax_items = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadTree($taxonomy);

// Inject into the options array
foreach($tax_items as $tax_item) {
  $options[$tax_item->tid] = $tax_item->name;
}

// Create a select form element
$form['taxonomy_options'] = array(
    '#type' => 'select',
    '#options' => $options,
    '#required' => TRUE,
);


Drupal 7

// Create empty options array
$options = array();

// Taxonomy vocabulary machine name
$taxonomy = 'tax_machine_name';

// Get all the taxonomy terms from the vocabulary
$vocab = taxonomy_vocabulary_machine_name_load('tax_machine_name')->vid;
$tax_items = taxonomy_get_tree($vocab);

// Inject into the options array
foreach($tax_items as $tax_item) {
  $options[$tax_item->tid] = $tax_item->name;
}

// Create a select form element
$form['taxonomy_options'] = array(
    '#type' => 'select',
    '#options' => $options,
    '#required' => TRUE,
);

 

Create a node in Drupal 8

Create a node in Drupal 8

Creating a node programmatically in Drupal 8? There is much more to it that this, but this is the start...

use \Drupal\node\Entity\Node;

$node = Node::create(['type' => 'CONTENT_TYPE']);
$node->set('title', 'YOUR TITLE');
// Simple textfield
$node->set('field_TEXT_FIELD', 'Your Text Here');
        
$node->save();