Drupal 11:如何将数据传输对象与队列 API 结合使用并拒绝无效队列项

Drupal 11:结合队列 API 使用数据传输对象

在将数据写入 Drupal 的队列数据库系统时,Drupal 会借助 PHP 的 serialize() 函数,把信息转换为字符串。而从数据库提取信息时,队列系统则会用 unserialize() 函数将数据反序列化,使其恢复成原始信息。

刚开始使用队列系统时,不少人可能会选择用数组或者 PHP 的 stdClass 对象来存储队列中的信息。虽说这种方法行得通,但它们所包含的信息形式过于自由,这就会让测试或处理数据变得有些棘手。

有一种更好的数据存储方式,那就是创建一个已知类型的对象,然后把它当作队列的存储介质。

在系统的不同部分之间使用对象来传递数据的这种技术,被称作数据传输对象(DTO)。借助 DTO,能让我们在整个应用程序中以统一的方式呈现数据。这是一种设计模式,它规范了特定数据的传递方式,避免了使用数组来完成同样任务时可能出现的问题。

在本文中,我公司将探讨如何创建一个用于 Drupal 队列 API 的 DTO,以及怎样通过拒绝队列中的无效项,来保护队列处理免受错误影响。在 Drupal 开发和 Drupal 模块开发过程中,合理运用这些技术能提升系统的稳定性和可维护性。

本文展示的所有代码都能在配套的 GitHub 仓库中找到,该仓库呈现了在 Drupal 中运行队列 API 的一些示例。

一、创建 DTO

在 PHP 里,DTO 其实就是一个普通的类,只不过关键区别在于,我们使用只读类(从 PHP 8.2 开始)语法,这意味着所有属性只能在构造函数中写入一次。这么做的目的是防止对象创建后,其中的数据被更改。

通常来讲,为对象创建接口是个不错的主意,这样可以确保程序的规范性。

<?php

declare(strict_types=1);

namespace Drupal\queue_class_example\Queue;

/**
 * QueueData 对象的接口。
 */
interface QueueDataInterface {

  /**
   * 获取 ID。
   *
   * @return int
   *   ID。
   */
  public function getId(): int;

}

现在我们就可以创建一个实现该接口的对象。

<?php

declare(strict_types=1);

namespace Drupal\queue_class_example\Queue;

/**
 * 以只读类的形式为队列处理程序存储信息。
 */
readonly class QueueData implements QueueDataInterface {

  /**
   * 队列项的 ID。
   *
   * @var int
   */
  protected int $id;

  /**
   * 创建一个 QueueData 对象。
   *
   * @param int $id
   *   队列项的 ID。
   */
  public function __construct(int $id) {
    $this->id = $id;
  }

  /**
   * 获取 ID。
   *
   * @return int
   *   ID。
   */
  public function getId(): int {
    return $this->id;
  }

}

要创建我们的对象,就像创建其他 PHP 类一样,直接创建一个新实例就行。

$data = new QueueData(123);

这里的关键区别在于,一旦对象创建完成,就无法再更改它。在上述示例中,ID 为 123 就永久设置给该对象了。

二、结合队列 API 使用 DTO

在队列系统中使用 DTO 很简单,我们只需实例化对象,再使用队列的标准 createItem() 方法将该项添加到队列中即可。在 Drupal11 的开发过程中,这种方式能让队列管理更加规范。

/** @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);
}

要是查看数据库,就会发现我们的类及其包含的数据已被序列化。

> 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)

这表明,当我们从队列中取回数据时,它将是一个完整的 QueueData 对象,里面包含着我们最初放入的数据。

三、拒绝队列中的项

使用这种技术有个好处,就是现在可以拒绝处理队列中任何不包含 QueueData 对象的项。我们可以利用 instanceof 类型运算符来实现这一点,如果对象不匹配,就拒绝处理队列项。

<?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;

/**
 * 队列类示例的队列工作器。
 *
 * @QueueWorker(
 *   id = "queue_class_example",
 *   title = @Translation("类队列示例的队列工作器。"),
 *   cron = {"time" = 60}
 * )
 */
class QueueExampleWorker extends QueueWorkerBase implements ContainerFactoryPluginInterface {
  use StringTranslationTrait;

  /**
   * 日志记录器工厂。
   *
   * @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)) {
      // 在 Drupal 定时任务队列处理程序中,从队列中移除项的唯一方法是静默返回。如果队列项没有错误,
      // 则定时任务会认为该项已处理完毕,因此会将其从队列中移除。这意味着如果此方法接收到一个不是
      // QueueDataInterface 对象的对象,我们只需记录错误并返回。
      $this->logger->error($this->t('无法处理队列项。'));
      return;
    }

    // 在此处处理队列项。
    // 记录该项已处理。
    $this->logger->info($this->t('已处理类队列项 @id', ['@id' => $data->getId()]));
  }

}

这看起来可能有些奇怪,但在 Drupal 定时任务队列处理程序中,从队列中移除项的唯一办法就是静默返回。要是队列项没有错误,定时任务就会认为该项已处理完,从而将其从队列中移除。也就是说,如果此方法接收到一个不是 QueueDataInterface 对象的对象,我们只需记录错误并返回。

四、结论

虽然这里没有添加任何自定义功能,但使用 DTO 能让我们把数据从表单传输到队列工作器,并确保工作器中存在正确的数据。使用数组或 stdClass 对象来存储队列数据是可行的,但使用合适的数据类型来传输数据会更加可靠,这在 Drupal11 的开发场景下显得尤为关键。

DTO 本身仅用于传输数据。所有数据库访问、实体创建或其他基于服务的方法都应通过 processItem() 方法来完成。这就是为什么该类被设置为只读,目的是防止设置除简单值之外的任何内容。

本文展示的所有代码都能在配套的 GitHub 仓库中找到,该仓库呈现了在 Drupal 中运行队列 API 的一些示例。若要安装,只需启用 queue_class_example 模块,然后使用相关页面上的表单用 DTO 项填充队列即可。在进行 Drupal 升级时,也可以参考这些代码示例来确保队列功能的正常运行。