Jun 11 2018
Jun 11

Three months ago I wrote an article on how to Create Image Styles and Effects programmatically and today we're following up on that article but introducing on how we can do that dynamically.

So, essentially what we would like to do is that we display an image, where we can adjust the way the image is outputted, given a height, width or aspect ratio etc.

Please bear in mind that all code provided in this article are experimental and does not yet cover things like access control, etc in this part.

Let's take a look at the service Unsplash.com. Its basically a free image bank with high quality images submitted by awesome freelancers and professionals that you can use for free.

Lake Tahio

Image by Eric Ward

The URL for the image above is the following:

https://images.unsplash.com/photo-1499365094259-713ae26508c5?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=26d4766855746c603e3d42aaec633144&auto=format&fit=crop&w=500&q=60

The parts we're actually interested in are: &auto=format&fit=crop&w=500&q=60 we can adjust them as we like and the image is displayed differently, i.e. changing the width of the earlier image to a smaller one:

Lake Tahio smaller

Alright, that's what we would like to do in Drupal 8. This article will be very iteratively, we'll rewrite the same code over and over until we get what we want. We'll notice issues and problems that we will deal with through out the article.

Prepare an environment to work in

We'll use a fresh Drupal 8.6.x installation.

To quickly scaffold some boilerplate code I'm going to use Drupal Console.

First let's create a custom module where we can put our code and logic in:

$ vendor/bin/drupal generate:module

I'll name the module dynamic_image_viewer

dynamic_image_viewer.info.yml

name: 'Dynamic Image Viewer'
type: module
description: 'View an image dynamically'
core: 8.x
package: 'Custom'

Next we need some images to work with, we'll use the core Media module for that. So let's enable that module:

vendor/bin/drupal module:install media

Now we can add some images. Go to Content >> Media >> Add media.

Media content

Implementing a Controller to display the image

The first step is to create a controller that will render the Media image to the browser. Again we'll use Drupal Console for a controller scaffold: vendor/bin/drupal generate:controller

We'll create a route on /image/{media} where Media will accept an media ID that due to Drupals parameter upcasting will give us a media instance in the controller method arguments. Doing this, if a invalid media ID is passed in the URL a 404 page is shown for us. Neat!

So we'll modify the generated controller slightly to this:

src/Controller/ImageController.php

<?php

namespace Drupal\dynamic_image_viewer\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\media\MediaInterface;

/**
 * Class ImageController.
 */
class ImageController extends ControllerBase {

  /**
   * Show an image.
   *
   * @param MediaInterface $media
   *
   * @return array
   */
  public function show(MediaInterface $media) {
    return [
      '#type' => 'markup',
      '#markup' => $media->id(),
    ];
  }

}


And the routing file looks like this: dynamic_image_viewer.routing.yml

dynamic_image_viewer.image_controller_show:
  path: '/image/{media}'
  defaults:
    _controller: '\Drupal\dynamic_image_viewer\Controller\ImageController::show'
    _title: 'show'
  requirements:
    _permission: 'access content'

If we install the module, vendor/bin/drupal module:install dynamic_image_viewer and hit the URL /image/1 we should see a page with the ID being outputted.

Render the original image

Ok. Currently nothing is rendered, so what we'll do is that we render the uploaded original image first.

To serve the file we'll use BinaryFileResponse. So let's update the ImageController::show method.

We'll also import the class in the top of the file:

use Symfony\Component\HttpFoundation\BinaryFileResponse;

  /**
   * Show an image.
   *
   * @param MediaInterface $media
   *
   * @return BinaryFileResponse
   */
  public function show(MediaInterface $media) {
    $file = $media->field_media_image->entity;

    $uri = $file->getFileUri();
    $headers = file_get_content_headers($file);

    $response = new BinaryFileResponse($uri, 200, $headers);

    return $response;
  }

So what we do here is that we grab the File entity from the field_media_image field on the Media image bundle. We get the URI and, using the file_get_content_headers we get the proper headers. Finally we serve the file back with the proper headers to the viewer.

And if we hit the URL again:

original-image-rendered

Before we continue, we should note some things that we'll get back to later:

  • What if the media ID is not a Media image?
  • The user can still access the media even if its unpublished.
  • What about cache?

Let's make a hard-coded image derivative

To modify the image, we'll create a new instance of ImageStyle and add an image effect.

Let's update the ImageController::show method again:

  /**
   * Show an image.
   *
   * @param MediaInterface $media
   *
   * @return BinaryFileResponse
   */
  public function show(MediaInterface $media) {
    $file = $media->field_media_image->entity;

    $image_uri = $file->getFileUri();

    $image_style = ImageStyle::create([
      'name' => uniqid(), // @TODO This will create a new image derivative on each request.
    ]);
    $image_style->addImageEffect([
      'id' => 'image_scale_and_crop',
      'weight' => 0,
      'data' => [
        'width' => 600,
        'height' => 500,
      ],
    ]);

    $derivative_uri = $image_style->buildUri($image_uri);

    $success = file_exists($derivative_uri) || $image_style->createDerivative($image_uri, $derivative_uri);

    $response = new BinaryFileResponse($derivative_uri, 200);

    return $response;
  }

So what we do here is that we create a new ImageStyle entity, but we don't save it. We give it a unique name (but we'll change that soon) and then add we add an image effect that scale and crops the image to a width of 600 and height 500.
And then we build the derivate uri and if the file exists already, we'll serve it and if not we'll create a derivative of it.

There is one big problem here. Since we use a unique id as name of the image style we'll generate a new derivative on each request which means that the same image will be re-generated over and over. To solve it for now, we could just change the

 $image_style = ImageStyle::create([
      'name' => uniqid(), // @TODO This will create a new image derivative on each request.

to a constant value, but I left it for that reason intentionally. The reason is that I want to explicitily tell us that we need to do something about that and here is how:

If we look back at the URI from Unsplash earlier &auto=format&fit=crop&w=500&q=60, these different keys are telling the code to derive the image in a certain way.

We'll use the provided keys and combine them some how in to a fitting name for the image style. For instance, we could just take the values and join them with a underscore.

Like so:

format_crop_500_60 and we'll have a unique string. If the user enters the same URL with the same parameters we'll be able to find the already existing derivative or if its another image, we'll create a derivative for it.

You'll also notice that I removed the $headers = file_get_content_headers($file); it is because those headers are not the correct ones for ur derivatives, we'll add them back soon.

Dynamic width and height values

On our second iteration of the code we'll now add the width and height parameters, and we'll also change the name of the image style to be dynamic.

Again, we'll update ImageController::show

We'll also import a class by adding use Symfony\Component\HttpFoundation\Request; in the top of the file.

  /**
   * Show an image.
   *
   * @param Request $request
   * @param MediaInterface $media
   *
   * @return BinaryFileResponse
   */
  public function show(Request $request, MediaInterface $media) {

    $query = $request->query;

    $width = (int) $query->get('width', 500);
    $height = (int) $query->get('height', 500);

    // We'll create the image style name from the provided values.
    $image_style_id = sprintf('%d_%d', $width, $height);

    $file = $media->field_media_image->entity;

    $image_uri = $file->getFileUri();

    $image_style = ImageStyle::create([
      'name' => $image_style_id,
    ]);
    $image_style->addImageEffect([
      'id' => 'image_scale_and_crop',
      'weight' => 0,
      'data' => [
        'width' => $width,
        'height' => $height,
      ],
    ]);
    
    // ... Rest of code

First we updated the method signature and injected the current request. Next, we'll get the width and height parameters if they exist and if not we fallback to something. We'll build an image style name of these dynamic values. With this we updated the name of the ImageStyle instance we create which makes sure that we can load the same derivative if the user hits the same URL. Finally we updated the width and height in the image effect.

Here is the updated ImageController::show and current file:

src/Controller/ImageController.php

<?php

namespace Drupal\dynamic_image_viewer\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\media\MediaInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Drupal\image\Entity\ImageStyle;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Image\ImageFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Class ImageController.
 */
class ImageController extends ControllerBase {

  /**
   * The image factory.
   *
   * @var \Drupal\Core\Image\ImageFactory
   */
  protected $imageFactory;

  /**
   * Constructs a ImageController object.
   *
   * @param \Drupal\Core\Image\ImageFactory $image_factory
   *   The image factory.
   */
  public function __construct(ImageFactory $image_factory) {
    $this->imageFactory = $image_factory;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('image.factory')
    );
  }
  /**
   * Show an image.
   *
   * @param Request $request
   * @param MediaInterface $media
   *
   * @return BinaryFileResponse
   */
  public function show(Request $request, MediaInterface $media) {

    $query = $request->query;

    $width = (int) $query->get('width', 500);
    $height = (int) $query->get('height', 500);

    $image_style_id = sprintf('%d_%d', $width, $height);

    $file = $media->field_media_image->entity;

    $image_uri = $file->getFileUri();

    $image_style = ImageStyle::create([
      'name' => $image_style_id,
    ]);
    $image_style->addImageEffect([
      'id' => 'image_scale_and_crop',
      'weight' => 0,
      'data' => [
        'width' => $width,
        'height' => $height,
      ],
    ]);

    $derivative_uri = $image_style->buildUri($image_uri);

    $success = file_exists($derivative_uri) || $image_style->createDerivative($image_uri, $derivative_uri);

    $headers = [];

    $image = $this->imageFactory->get($derivative_uri);
    $uri = $image->getSource();
    $headers += [
      'Content-Type' => $image->getMimeType(),
      'Content-Length' => $image->getFileSize(),
    ];

    $response = new BinaryFileResponse($uri, 200, $headers);

    return $response;
  }

}

First we added a new dependency to our controller \Drupal\Core\Image\ImageFactory which allows us to construct an Image instance, where we can get meta data from the image, but also gives us a unified interface to apply things to our image. For instance, we could desaturate the image by doing $image->desaturate(); and then resave the file. Fow now we're only using it to retrieve the meta data. We'll take advantage of that in the next part, when we refactor some of the written code and add more flexibility to what we can dynamically output.

If we hit the url and add both the width and height parameters we'll get something like this:

generated-image

In the up coming article we'll take a better look at what we have, what we miss (access control, what if a user hits the same URL at the same time), adding more effects, and exploring the use of the Image and toolkit APIs more in depth.

We'll most likely remove adding image effects through ImageStyles and only use the image style for creating derivates that we can we can later apply changes with the toolkit API.

If you want to continue on your own, take a look at ImageStyleDownloadController.php file in core which contains a lot of code that we can re-use.

Mar 28 2018
Mar 28

In our Drupal 7 site we have an enhanced textfield that autocompletes already stored values. When migrating to Drupal 8 we could normalize this by using a vocabulary and terms instead.

So in our current Drupal 7 setup we have something like:

Content type: Employee

nid name field_role 1 John Web developer 2 Emil Web developer 3 Henrik Web designer 4 Karl Web designer 5 David Sales 6 John Sales

And in Drupal 8 we want this instead:

Vocabulary: Role

tid title 1 Web developer 2 Web designer 3 Sales

Content type: Employee

nid name field_role 1 John 1 2 Emil 1 3 Henrik 2 4 Karl 2 5 David 3 6 John 3

Using entity_generate process plugin

The first approach we can take is to use the entity_generate plugin provided by Migrate Plus module.

In our migration file:

process:
  field_role:

    # Plugin to use
    plugin: entity_generate

    # Field from source configuration
    source: field_role

    # Value to compare in the bundle
    value_key: name

    # Bundle key value
    # If you get errors consider using only bundle
    bundle_key: vid

    # Bundle machine name
    bundle: role

    # Type of entity
    entity_type: taxonomy_term

    # Set to true to ignore case on lookup
    ignore_case: true

Running this migration will take the value of the textfield and try to lookup the term by name on the destination and if it does not exist the term will be created.

In the second part of this article (coming soon) we will take a look at how we can deal with translations.

Mar 05 2018
Mar 05

In this short article we will learn how to create image styles and how to add image effects programmatically.

Creating image styles

Image styles are stored as configuration entities in Drupal 8. So to create a new image style we simply need to create a ImageStyle instance and save it.

<?php

use Drupal\image\Entity\ImageStyle;

$imageStyle = ImageStyle::create([
  'name' => 'machine_name',
  'label' => 'Label',
])->save();

Add image effects

We will use the ImageStyle::addImageEffect method to do this.

$imageStyle->addImageEffect([
  'id' => 'image_scale_and_crop',
  'weight' => 0,
  'data' => [
    'width' => 500,
    'height' => 500,
]);
$imageStyle->save();

Each image effect is a plugin of the ImageEffect plugin type. You can find the core's image effect plugins under the Drupal\image\Plugin\ImageEffect namespace in the image module.

Feb 19 2018
Feb 19

In Drupal 7 we could use Views Selective Filters module to have an exposed filter only show options that belong to result set. The module has not been ported to Drupal 8 yet. So what do we do?

Here is our current view:

article-view

As we can see there is no node with the JavaScript tag and therefore we would like to remove that option from the exposed filter.

Ideally we would like a setting that we can configure on the filter, however I did not have the time to implement that. Instead we will make a trade off and alter in the exposed form directly.

First we need somewhere to put the code, so we'll create a custom_views module. I'll use Drupal Console to scaffold this out quickly. In custom_views.module we'll add our code, I've added additional comments to explain what we do.

<?php

/**
 * @file
 * Contains custom_views.module.
 */

use Drupal\Core\Database\Database;

/**
 * Implements hook_form_FORM_ID_form_alter().
 */
function custom_views_form_views_exposed_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state)
{
  // If the exposed filter does not exist on this form, there's nothing we can do here.
  if (!array_key_exists('field_tags_target_id', $form)) {
    return;
  }

  // Options are tag entity id => title.
  $options = $form['field_tags_target_id']['#options'];

  // We are querying for tags belonging to at least one node.
  // We group by tag id so we don't get a result for each
  // node the tag is referred by.
  // We also set a condition on the bundle, as we could have
  // other bundles using same field.
  $connection = Database::getConnection();
  $sth = $connection->select('node__field_tags', 'tags');
  $sth->addField('tags', 'field_tags_target_id');
  $sth->condition('bundle', 'article');
  $sth->groupBy('tags.field_tags_target_id');

  $data = $sth->execute();
  // Flip the result set so the array key is the tag entity id.
  $results = array_flip($data->fetchAll(\PDO::FETCH_COLUMN, 'field_tags_target_id'));

  // Intersects the arrays, giving us back an "filtered" array.
  $options = array_intersect_key($options, $results);

  // Replace the options.
  $form['field_tags_target_id']['#options'] = $options;
}

article-view-filtered

That's it! We could definitely improve this by breaking out the logic so we can reuse for additional field or even better, integrate this functionality better in Views (for instance by porting Views Selective Filter to D8 https://www.drupal.org/project/views_selective_filters/issues/2660844)

To quote my colleagues:

Good Enough for Now and Safe Enough to Try

Oct 04 2017
Oct 04

In our modules we can include default configuration to ship content types, views, vocabularies on install. If we try to reinstall the module, we'll get an error. Let's take a look at why and how we can solve it.

Unable to install … already exist in active configuration

During uninstall the configuration is not removed because it has no connection to the module itself and thus we get the error because Drupal modules may not replace active configuration. The reason for that is to prevent configuration losses.

We can move all the configuration to config/optional rather than config/install which basically means that configuration that does not exists will be installed and the existing one will be ignored. If we do this we can reinstall the module.

So, if we want the configuration to remain active we've already solved the problem, but if we don't want that, we want the module to remove all configuration and contents its provided we'll need to look at another solution.

Let's take a look at the Article content type that is created when using the Standard distribution on installing Drupal.

node.type.article.yml

langcode: en  
status: true  
dependencies: {  }  
name: Article  
type: article  
description: 'Use <em>articles</em> for time-sensitive content like news, press releases or blog posts.'  
help: ''  
new_revision: true  
preview_mode: 1  
display_submitted: true  

field.field.node.article.body.yml

langcode: en  
status: true  
dependencies:  
  config:
    - field.storage.node.body
    - node.type.article
  module:
    - text
id: node.article.body  
field_name: body  
entity_type: node  
bundle: article  
label: Body  
description: ''  
required: false  
translatable: true  
default_value: {  }  
default_value_callback: ''  
settings:  
  display_summary: true
field_type: text_with_summary  

If we delete the content type, we can find out that the field instance body on the content type has been removed. First we actually see that the configuration will be removed when confirming the deletion of the content type, but also by looking in the database in the config table where active configuration is stored. It's gone.

The reason it's deleted is because it has an dependency on the node.type.article.yml configuration as we can see:

dependencies:  
  config:
    - field.storage.node.body
    - node.type.article
  module:
    - text

So what we need to do to make sure the content type we create when installing or module, that it's configuration uses our module as an dependency. So let's take a look at how we can do that:

Let's imagine we have a custom_event module that creates a event content type.

node.type.event.yml

langcode: en  
status: true  
dependencies:  
  enforced:
    module:
      - custom_event
third_party_settings: {}  
name: Event  
type: event  
description: 'Event'  
help: ''  
new_revision: true  
preview_mode: 1  
display_submitted: false  

The dependencies-part is the interesting one:

dependencies:  
    enforced:
        module:
            - custom_event

We have defined a custom_event module, in that module we have some exported configuration files in the config/install folder. We update the node.type.event.yml configuration file to have our module as an dependency. Now when we uninstall the module, the content type will be removed.

We also have to do this for our views, taxonomy, and field storages, or pretty much any configuration entity we provide configuration for. We don't have to worry about field instances, as we saw above those are dependent on the content type itself, but field storages on the other hand does not depend on a content type because you can reuse fields on multiple of those.

So, just add the module as an dependency and you're good to go, here's an example on a field storage field_image

field.storage.node.field_image.yml

langcode: en  
status: true  
dependencies:  
  enforced:
    module:
      - custom_event
  module:
    - file
    - image
    - node
id: node.field_image  
field_name: field_image  
entity_type: node  
type: image  
settings:  
  uri_scheme: public
  default_image:
    uuid: null
    alt: ''
    title: ''
    width: null
    height: null
  target_type: file
  display_field: false
  display_default: false
module: image  
locked: false  
cardinality: 1  
translatable: true  
indexes:  
  target_id:
    - target_id
persist_with_no_fields: false  
custom_storage: false  

Please enable JavaScript to view the comments powered by Disqus.

Jan 04 2017
Jan 04

Cron is used to perform periodic actions. For example you would like to:

  • Send a weekly newsletter every Monday at 12:00 a.m.
  • Create a database backup once per day.
  • Publish or unpublish a scheduled node.
  • Send reminder emails to users to activate their accounts.

... or some other task(s) that has to be automated and run at specific intervals.

Cron in Drupal

Cron configuration can be found at Administration > Configuration > System > Cron

alt

What tasks does Drupal perform when cron is run?

This depends entirely on what modules you have enabled and use of course, but here are some pretty usual examples on what tasks are run in cron:

  • Updating search indexes for your search engine when using Search core module.
  • Publishing or unpublishing nodes when using the Scheduler module.
  • If you have Update Manager module enabled, a task is run to look for updates. It also sends an email if you configured it to do so.
  • If you have dblog (Database logging) enabled this task deletes messages after a set limit.
  • Temporary uploaded files are deleted by the File module.
  • Fetch aggregated content when using Aggregator module.

Running cron

First we have the Automated Cron core module (sometimes referred as Poor man's cron) which during a page request checks when cron was last run and if it has been to long it processes the cron tasks as part of that requests.

alt

Cron is set to run every third hours.

There are two things to consider when using this approach. If no one visits your website the cron doesn't run. Secondly, if the website is complex or the cron tasks are heavy the memory can exceed and slow down the page request.

The second approach is to actually setup a cron job that runs at the intervals you specify. Configuring this up depends on what system you use, but typically isn't that hard to do. If you use a shared host it's most likely you can do that right off in your control panel, and if you have your own server you can use the crontab command.

Read the Configuring cron jobs using the cron command on drupal.org for more details.

Implementing Cron tasks in Drupal

Cron tasks are defined by implementing the hook_cron hook in your module, just like in previous Drupal versions.

/**
 * Implements hook_cron().
 */
function example_cron() {  
  // Do something here.
}

And that's pretty much it. Rebuild cache and next time cron runs your hook will be called and executed.

There are a couple of things we have to take in to consideration:

When did my Cron task run the last time?

One way to remember that is using State API which stores transient information, the documentation explains it as such:

It is specific to an individual environment. You will never want to deploy it between environments.
You can reset a system, losing all state. Its configuration remains.
So, use State API to store transient information, that is okay to lose after a reset. Think: CSRF tokens, tracking when something non-critical last happened …

With that in mind, we could do something like:

 $last_run = \Drupal::state()->get('example.last_run', 0);

  // If 60 minutes passed since last time.
  if ((REQUEST_TIME - $last_run) > 3600) {
    // Do something.

    // Update last run.
    \Drupal::state()->set('example.last_run', REQUEST_TIME);
  }

To ensure our task is only run once per hour. Again though, if our Cron is set to run in a periodic longer than one hour it won't run every hour. (Who could have guessed that?) If you use Automatic cron and have no activity during some hours, the cron won't be run then as well.

How time consuming is my task?

Operations like deleting rows from a table in the database with timestamp as condition is pretty light task and can be executed directly in the hook_cron implementation. Like so:

 // Example from the docs.
 $expires = \Drupal::state()->get('mymodule.last_check', 0);
  \Drupal::database()->delete('mymodule_table')
    ->condition('expires', $expires, '>=')
    ->execute();
  \Drupal::state()->set('mymodule.last_check', REQUEST_TIME);

But if you have to run tasks that takes time, generating PDF, updating a lot of nodes, import aggregated content and such you should instead use something called QueueWorkers which lets you split up the work that needs to be done in to a queue that can later be processed over the course of later cron runs and prevents that a single cron eventually fails due to a time out.

QueueWorkers and Queues

So, we have a long-running task we want to process. As mentioned earlier we shouldn't just put all the processing into the hook as it can lead to timeouts and failures. Instead we want to split up the work into a queue and process them. The queues will later be processed in a later cron.

So let's pretend we've created a site where user can subscribe to things and when they do, they get an email sent with an attached PDF, for the sake of the example we'll also send emails to the admins that someone subscribed. Both sending emails and generating PDF are long running tasks especially if we are doing them at the same time, so let's add those items to an queue and let a queue worker process it instead.

To add a queue, we first get the queue and then add the item to it:

// Get queue.
$queue = \Drupal::queue('example_queue');

// Add some fake data.
$uid = 1;
$subscriber_id = 2;
$item = (object) ['uid' => $uid, 'subscriber_id' => $subscriber_id];

// Create item to queue.
$queue->createItem($item);

So we get an queue object by a name, a name which is later used to identify which Queue Worker that should process it. And then we add an item to it by simply calling the createItem method.

Next we'll have to create a QueueWorker plugin. The QueueWorker is responsible for processing a given queue, a set of items.

Let's define a plugin with some pseudo long running task:

modules/custom/example_queue/src/Plugin/ExampleQueueWorker.php:

<?php  
/**
 * @file
 * Contains \Drupal\example_queue\Plugin\QueueWorker\ExampleQueueWorker.
 */

namespace Drupal\example_queue\Plugin\QueueWorker;

use Drupal\Core\Queue\QueueWorkerBase;

/**
 * Processes tasks for example module.
 *
 * @QueueWorker(
 *   id = "example_queue",
 *   title = @Translation("Example: Queue worker"),
 *   cron = {"time" = 90}
 * )
 */
class ExampleQueueWorker extends QueueWorkerBase {

  /**
   * {@inheritdoc}
   */
  public function processItem($item) {
    $uid = $item->uid;
    $subscrition_id = $item->subscription_id;

    $user = \Drupal\user\Entity\User::load($uid);

    // Get some email service.
    $email_service = \Drupal::service('example.email');

    // Generate PDF
    $subscriber_service = \Drupal::service('example.subscriber_pdf');
   $pdf_attachment = $subscriber_service->buildPdf($subscriber_id, $user);

    // Do some stuff and send a mail.
    $emailService->prepareEmail($pdf_attachment);
    $emailService->send();

    $emailService->notifyAdmins($subscriber_id, $user);
  }

}

So let's break it down.

We use the Annotation to tell Drupal it's a QueueWorker plugin we created.

/**
 * Processes tasks for example module.
 *
 * @QueueWorker(
 *   id = "example_queue",
 *   title = @Translation("Example: Queue worker"),
 *   cron = {"time" = 90}
 * )
 */

The id argument is the most important since it must match the machine name of the queue we defined earlier.

The cron argument is optional and basically tells Drupal that when the cron is run it should spend maximum this time to process the queue, for this example we used 90 seconds.

Then we implement the public function processItem($item) { method which will pass the data we gave for each item when we created the queue.

In the pseudo example I'm loading the user uid we passed in to the queue item and then getting 2 services which one generates a PDF (pretty heavy operation) and the second one that supposedly later emails it. We then send emails to all the admins through the notifyAdmins method. So that was pretty simple. We simply create a new plugin class, use the Annotation to tell Drupal its a plugin and then implement the method which gets the data from where we added the item to the queue.

For this example we just added some operation to be processed in a queue that doesn't necessarily belong in the cron hook but instead when the user actually subscribed for something. So what I'm essentially saying here is that you don't need to create a queue in a cron hook, but can do that anywhere in your code.
In practise its the same thing, you get the queue $queue = \Drupal::queue('example_queue') and then add item to the queue $queue->createItem($data) and then define ourselves a QueueWorker which then processes the queue items when cron is run.

So the question we should ask ourselves here: Should we add individual tasks to a queue and let cron process it? And the answer - it depends. If the task slows down the request and keeps the user waiting, it's definitely something to consider. These things may be a better case for using something like a Background job, but you may not always be able to do that (and nothing that comes out of the box in Drupal) and if so a cron will take of some significant time from the request so it's not too slow for the user (..or timeouts for that matter).

Here's all the code without the pseudo code that you can use as boilerplate:

<?php  
/**
 * @file
 * Contains \Drupal\example_queue\Plugin\QueueWorker\ExampleQueueWorker.
 */

namespace Drupal\example_queue\Plugin\QueueWorker;

use Drupal\Core\Queue\QueueWorkerBase;

/**
 * Processes tasks for example module.
 *
 * @QueueWorker(
 *   id = "example_queue",
 *   title = @Translation("Example: Queue worker"),
 *   cron = {"time" = 90}
 * )
 */
class ExampleQueueWorker extends QueueWorkerBase {

  /**
   * {@inheritdoc}
   */
  public function processItem($item) {
  }

}

For a real example, take a look at the Aggregator module which uses Cron and QueueWorkers.

Please enable JavaScript to view the comments powered by Disqus.

Dec 13 2016
Dec 13

It’s been a bit more than a year since Drupal 8 was released. There are more than 15 000 open issues open (and 70 000 closed (!), just to give you some context) in the Drupal core issue queue. Here’s a few of those issues I’m keeping an extra eye on.

Changing password hashing library.

Replace custom password hashing library with PHP 5.5 password_hash()

This issue has been active since 4 years back and is still actively going. The idea is to replace the current password library which is a forked version of phpass to use the built-in password_hash functionality that was introduced in PHP 5.5.
cweagans who created the issue lifts up three main problems with the current solution:

  • It has to be maintained by Drupal
  • It has 0% interoperability
  • It lacks forward-upgrading mechanism

and I agree with all of them. Drupal itself is a huge code base and has a lot of things to keep updated and further developed. Password hashing algorithms are hard and definitely takes an enormous amount of time to get it right. So, instead why not use something that’s built in to PHP and is secure.

The interoperability concern also makes sense. Because it is built in, it’s secure by design (by the way I’m not really saying the current solution is insecure) and are used in different systems and projects outside of Drupal. This also allows us to easier migrate something to Drupal without having to worry about rehashing passwords when doing so.

The final concern is about forward-upgrading. So currently bcrypt hashing algorithm is the to-go strategy for password hashing. If there comes a better algorithm in the future or you change the cost of the algorithm. The verify will still pass, but you can in a few lines make sure the password gets rehashed for enhanced security.

Menu subtrees in menu blocks show all subitems regardless of the active menu item

This issue is about 1 year old and is something I’ve experienced and had to deal with in a few of my latest projects.

So the problem is that if you have a menu which looks like this:

- Home
- Products
 - Product category 1
   - Product 1
   - Product 2
 - Product category 2
   - Product 3
   - Product 4

If we set the Menu block to level 3 and visits Product category 1 we are expected to see:

   - Product 1
   - Product 2

However currently it renders like:

- Product 1
- Product 2

- Product 3
- Product 4

To solve this temporarily, we’ve used the Menu block module along with an patch provided by an patch: https://www.drupal.org/node/2811337 (which is now closed due to duplicating this core issue).

Issue: https://www.drupal.org/node/2831274

I am most excited about this one. The idea is to bring the Media entity module into core. You should take a look at the module, but also the Drupal 8 Media guide.

But to give you an quick idea on what it is all about, it’s basically an initiative to add proper Media handling in Drupal. There are a lot of submodules that can be put together to give you a really powerful system for handling media. Such as entity browsers, image cropping, etc.

Currently we just use the plain simple file handling provided by Drupal today along with the IMCE module to reuse already uploaded files. It works fine, but I believe this initiative will give us something much more flexible and robust. Something we should expect from a CMS such as Drupal. I by mentioned this at work earlier this week and was told by a colleague this hopes to get into Drupal 8.4 so we’ll just have to idle by and see.

I’m also interested to learn how the upgrade path from using the module as contrib into core will look like. But also too see if there’s a good way to migrate existing media contents into this Media system.

Language support in Paragraphs

This one isn’t part of the Drupal core but as we use Paragraphs in most of our projects I’d also like to mention this issue:

Support translatable paragraph entity reference revision field

So currently Paragraphs does not have translation support and this way a Oh shit moment for us when creating a multilingual site. Luckily for us, this issue been active for a while and there are patches which works, as in it runs just fine in production.

Paragraphs is an awesome module for flexible content layout so getting this issue done is a deal-breaker for us. And again, these guys are making great progress and I wouldn’t be surprised if it was completed in a not so far future.

Dec 11 2016
Dec 11

Last week a friend called me and asked for help to put together some furnitures for his newly bought apartment. Together we tried to follow the user manual and as the time passed the more frustration we felt. Perhaps our clients feel the same way as we did when they use our systems? Somethings that feels clear to us (the builders) can seem like a impossibility for the consumer, the client.

With this analogy in mind I realised how important writing good documentation is. I also started thinking on how we can improve existing documentation and how to write it better for the future.

There are two kind of documentations I believe should be written, one for the consumers (the clients) and one for the developers.

Documentation for the consumers.

So, how can we best teach a client? For one we can have a user manual, introducing the system and the things clients will work with on a daily basis. Answering simple things like login, writing content, but also how the system works and should best be used. We use Drupal (a CMS) at work and they have a user manual that we translated and use as a base to introduce to the clients the basics of the CMS.

However many of the systems that are being developed are different than the core install and might be slightly different. Therefor if the CMS manual does not match that product, it should be adjusted to be so.

  • How is the system used best?
  • What are the limitations?
  • Workflows typical for the solution

Many of these questions has already been partly (or fully) discussed during sprint plans and discussions with the client on what you actually are producing. Putting them down in text, will help later on.

One of my colleagues also does video tutorials that are much appreciated from the clients. These videos are made in a local environment of the system that’s later shipped to production. Producing these videos as new features is shipped and how they are used is something I believe can be really useful. Videos also has the pro’s of the consumer being able to see how a workflow works from start to finish and jumping back and forth. In text it’s a bit more work and harder to do so.

We also like to invite clients to come over for a couple of hours to be introduced to the system face to face and being able to answer questions they might have right of the bat. These are often appreciated and I believe this might be one of the best ways to learn the client how to work with the system. This might not always be available to do, but if so, consider doing a Stream through something like Google Hangout or Skype. Recording these and making it available afterwards so we can look back is also an good idea.

Documentation for the developers.

Every once and then we have to go back on these medieval systems that was written years ago. Most often it’s a bug or even a new feature that has to be addressed. These legacy applications often tends to contain bad decisions and smelly code that is really hard to maintain. Perhaps the employee who first created is no longer with us and those who were around doesn’t have a clue on what’s going on it. These things are scary to work with, things break easily and there are no tests to verify that the changes you did broke something else in the system. Documentation for developers are also important and stuff that makes sense now will most certainly not in one year or more later on.

What about refactoring the code? Well refactoring and improving existing code is not always an option. We wish, but there may not be an budget or interesting in doing so.

To prevent these things we introduced Tech leads at work that makes sure documentation exists and is available to answer questions about the system when needed. The questions vary, but we’ve set up a pretty decent template:

  • Project details
    • CMS/Framework used?
    • Language, language version?
    • Web host?
    • Teach leads?
  • Introduction, small introduction on what the project is and what it does.
  • Startup, how do you install and get the project up and running on a local machine
  • Things to really know about this system
  • System structure
  • Production environment and backup routines
  • Things to verify works after an update (unless we have tests)
  • Sprint descriptions (what we did and were done in latest sprints)

I believe one of the most important thing is the Things to really know about this system. That is really helpful, especially if the system does something like pushing tweets, sending emails or something else that you have to be aware of. We kinda try avoiding these things and making sure development environment is safe to work in, but in legacy or projects that we’ve taken over it’s not always for granted.

Dec 07 2016
Dec 07

Currently the core module Contact form only gives you an accessible URL that you can visit too see your form. There are plenty use cases where you instead would like to embed it into a page, a node, etc.

First we have the Contact Block module which allows you to use blocks to display contact forms.

The second option is to do it programmatically. So for example if we would like to add a form to all pages, we could preprocess the page hook_preprocess_page(&$variables) and then:

  // Get feedback form to be rendered on all pages.
  $message = \Drupal::entityTypeManager()
          ->getStorage('contact_message')
          ->create(array(
          'contact_form' => 'feedback', // The machine name of the form.
      ));
  $form = \Drupal::service('entity.form_builder')->getForm($message);
  $vars['feedback_form'] = $form;

Now we have a feedback_form variable we can simply output in our page.html.twig template.

If you want the form to be dynamic without a page reload, take a look at the Contact ajax module which works just fine even though it's in beta!

Please enable JavaScript to view the comments powered by Disqus.

Dec 06 2016
Dec 06

By default Drupal provides a /node page http://example.com/node that lists all content. Unless you use that page as your front page it's a good idea to disable it.

Go to Structure --> Views --> Frontpage (Content)

alt

alt

Then press the Save button. And boom, you're done!

Removing /node programmatically

You could also write a custom module that removes the route programmatically. This is a good idea if you want some more flexibility, like you want a route only to be enabled on development environment and you don't want to adjust configuration back and forth. So having a module for altering routes is a pretty good idea. We'll take a look at that another time, but here's how to achieve the same thing as above with a custom module:

If you're not familar with creating modules, you could start off by reading Creating custom modules in the official Drupal documentation.

So we'll start off by creating a new module: I'll call mine custom_dev and I'll put it in modules/custom/custom_dev

Next we'll tell Drupal about the module by creating a custom_dev.info.yml which contains:

name: Custom dev  
type: module  
description: Development module  
core: 8.x  
package: Custom  

To alter routes we'll have to define a RouteSubscriber which listens to route events.
Let's create a custom_dev.services.yml

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

Finally we'll create the RouteSubscriber class. So in custom_dev/src/Routing we'll create a RouteSubscriber.php file:

<?php

namespace Drupal\custom_dev\Routing;

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

/**
 * Class RouteSubscriber.
 *
 * @package Drupal\custom_dev\Routing
 * Listens to the dynamic route events.
 */
class RouteSubscriber extends RouteSubscriberBase {  
  /**
   * {@inheritdoc}
   */
  protected function alterRoutes(RouteCollection $collection) {
    // Remove /node page route.
    $collection->remove('view.frontpage.page_1');
  }
}

That's it! Enable the module and /node will now be disabled.
If we would like to instead show a Permission denied, we could replace:

    // Remove /node page route.
    $collection->remove('view.frontpage.page_1');

with:

// Deny /node page access.
if ($route = $collection->get('view.frontpage.page_1')) {  
    $route->setRequirement('_access', 'FALSE');
}

Learn more about Drupal 8 routing

Here are some good links to learn more about the routing system:

Please enable JavaScript to view the comments powered by Disqus.

Dec 06 2016
Dec 06

So we released our first multilingual site a couple of weeks ago. And it still runs just fine! Throughout the journey there was a couple of issues we had to deal with and I'm going to walk you through each of them and tell you how we tackled them.

Do your homework

Not every contrib module does (or will) have translation support. Make sure you do some research and see that the module you need has it. Look through the issue queue and even better, play around a bit with it.

We had our Oh shit! moment when we realized Paragraphs did not have proper translation support yet. Luckily, there was an active issue that provided a working patch. For the record this patch is still on going and everyone contributing on it does some really awesome work.

So make sure you do some initial research to prevent you from ending up in a tricky situation later on in the process!

Our site has 2 languages and one of them has way more content than the other. The menu does not currently support or let you configure to hide these untranslated menu items. There's an on going issue but there are many aspects and different use cases to take into consideration.

For our use case we only want to show content in the current active language. We ended up using Content Language Access module which worked really well.

Do not change default language

Even though I wasn't able to reproduce the issue on a fresh Drupal 8 install but I believe it's worth mentioning anyways. Be careful when changing default language, especially if you're quite far in the development/staging phase (like some content been populated etc). We changed default language from English to Swedish, and I think that Drupal at some point missed something out which ended up in weird issues like not being able to translate some configuration. We ended up changing back and had to do some manual work like making sure that the exported configuration (through Configuration Manager) had the right langcode.

Drupal actually explicitly tells you that changing default language can be a bad idea. Instead you should change the language detection to use the other language you wish to have as a default language and you should be fine.

Filtering on a language neutral taxonomy in Views

We found an core bug when we were working on creating a listing of articles with Views.
We were only able to filter on the default language but not on our secondary language. You can read more on the Language Neutral Taxonomy cannot be filtered on issue.

Please enable JavaScript to view the comments powered by Disqus.

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web