Drupal 11: Using Data Transfer Objects With The Queue API

When writing data to the queue database system Drupal will convert the information to a string using the PHP serialize() function. When the information is pulled out of the database the queue system will unserialize() the data to convert it back into the original information.

When you first start using the queue system you will probably use an array or the PHP stdClass object to store the information on your queue. Whilst this works, the information they contain is pretty free form and makes testing or working with the data a little cumbersome.

A much better way of storing data is by creating an object of a known type and using that as the storage medium for the queue.

This technique of using an object to pass data around different parts of your system is known as Data Transfer Objects (DTO). This allows you to present data in a unified way across your application. This is a design pattern that standardizes how a particular bit of data is passed around, without having to resort to using arrays to accomplish the same job.

In this article we will look at creating a DTO for use in the queue API in Drupal, and how the use of DTOs can protect our queue processing from errors by rejecting items from the queue.

All of the code seen in this article is available on the accompanying GitHub repository that shows a few examples of running the Queue API in Drupal.

Creating A DTO

A DTO in PHP is just a normal class, the key difference is that we use the readonly class (since PHP 8.2) syntax, which means that all properties can only be written once (in the constructor). We do this to prevent the data in the object from being altered after it is created.

It is generally a good idea to create interfaces for our objects so that we can check to make sure that

<?php

declare(strict_types=1);

namespace Drupal\queue_class_example\Queue;

/**
 * Interface for the QueueData object.
 */
interface QueueDataInterface {

  /**
   * Get the ID.
   *
   * @return int
   *   The ID.
   */
  public function getId(): int;

}

We can now create an object that implements this interface.

<?php

declare(strict_types=1);

namespace Drupal\queue_class_example\Queue;

/**
 * Stores information as a readonly class for the queue handler.
 */
readonly class QueueData implements QueueDataInterface {

  /**
   * The ID of the queue item.
   *
   * @var int
   */
  protected int $id;

  /**
   * Creates a QueueData object.
   *
   * @param int $id
   *   The ID of the queue item.
   */
  public function __construct(int $id) {
    $this->id = $id;
  }

  /**
   * Get the ID.
   *
   * @return int
   *   The ID.
   */
  public function getId(): int {
    return $this->id;
  }

}

To create our object we just need to create a new instance of it, just like any other PHP class.

$data = new QueueData(123);

The key difference here is that once the object has been created we cannot change it again. In the above example, the ID of 123 is now set to that object permanently.

Using DTOs With The Queue API

Making use of the DTO within our queue system is pretty simple, we just need to instantiate the object and use the standard createItem() method of the queue to add the item to the queue.

/** @var \Drupal\Core\Queue\QueueInterface $queue */
$queue = \Drupal::service('queue')->get('queue_class_example');

for ($i = 0; $i < 100; $i++) {
  $item = new QueueData($i);
  $queue->createItem($item);
}

If we look in the database we can see that our class is serialized with the data it contains.

> select * from queue where name = 'queue_class_example' limit 1\G
*************************** 1. row ***************************
item_id: 1
   name: queue_class_example
   data: O:42:"Drupal\queue_class_example\Queue\QueueData":1:{s:5:" * id";i:0;}
 expire: 0
created: 1735389966
1 row in set (0.000 sec)

This means that when we get the data back from the queue it will be a full QueueData object containing the data that we put in there to begin with.

Rejecting Items From The Queue

One the benefits of using this technique is that you can now reject any item from the queue processing that doesn't contain a QueueData object. We can do this using the instanceof type operator and rejecting the processing of the queue item if the object doesn't match.

<?php

declare(strict_types=1);

namespace Drupal\queue_class_example\Plugin\QueueWorker;

use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\queue_class_example\Queue\QueueDataInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Queue worker for the queue_class_example.
 *
 * @QueueWorker(
 *   id = "queue_class_example",
 *   title = @Translation("Queue worker for the class queue example."),
 *   cron = {"time" = 60}
 * )
 */
class QueueExampleWorker extends QueueWorkerBase implements ContainerFactoryPluginInterface {
  use StringTranslationTrait;

  /**
   * Logger factory.
   *
   * @var \Drupal\Core\Logger\LoggerChannelInterface
   */
  protected $logger;

  /**
   * {@inheritDoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    $instance = new self($configuration, $plugin_id, $plugin_definition);
    $instance->logger = $container->get('logger.channel.queue_class_example');
    return $instance;
  }

  /**
   * {@inheritDoc}
   */
  public function processItem($data) {
    if (!($data instanceof QueueDataInterface)) {
      // The only way to remove an item from a queue inside the Drupal cron
      // queue handler is to return silently. If the queue item had no error
      // then cron sees the item as having worked and so it is removed from the
      // queue. This means that if this method receives an object that isn't
      // a QueueDataInterface object then we simply log the error and return.
      $this->logger->error($this->t('Unable to process queue item.'));
      return;
    }

    // Process the queue item here.
    // Log the item as having been processed.
    $this->logger->info($this->t('Processed class queue item @id', ['@id' => $data->getId()]));
  }

}

This might seem a bit odd, but the only way to remove an item from a queue inside the Drupal cron queue handler is to return silently. If the queue item had no error then cron sees the item as having worked and so it is removed from the queue. This means that if this method receives an object that isn't a QueueDataInterface object then we simply log the error and return.

Conclusion

Whilst we haven't added any custom functionality here the use of DTO has enabled us to transport data from the form to the queue worker and ensure that the correct data is in place in the worker. The use of arrays or stdClass objects for the queue data is fine, but it's much more reliable to use a proper datatype to transport the data.

The DTO itself should not be used for anything but transporting the data. All of the database access, entity creation, or other service based methods should be done via the processItem() method. This is why the class was set up to be readonly, which prevents anything more than simple values being set.

All of the code seen in this article is available on the accompanying GitHub repository that shows a few examples of running the Queue API in Drupal. To install, just enable the queue_class_example module and use the form at "/drupal-queue-examples/queue-class-example" to fill the queue with DTO items.

More in this series

Add new comment

The content of this field is kept private and will not be shown publicly.