Skip to main content

Adjusting a node's published time when publishing through content moderation

Published on

I use the Content Moderation module on my personal site to managed my publication workflow. One of my pet peeves if when I accidentally forget to adjust a blog post's creation timestamp once published. Sometimes it is only a few hours old, sometimes it is a week. I finally sat down to fix this small annoyance. Only to find out fixing it wasn't as simple as I would have thought.

Note: Jonathan Shaw has pointed me to the issue Dispatch events for changing content moderation states, which would solve this workaround I implemented.

I am fairly used to working with the State Machine module, as it is a backbone in Drupal Commerce for order workflows. We have Workflows, States, and Transitions in State Machine, along with a field type. Whenever a state field changes, it must fit into an available transition. We then dispatch events to the system for these transitions. 

The Drupal core modules Workflows has the same concept of Workflows, States, and Transitions. However, the Content Moderation module adds a UI and field components for Workflows. There are also no events. I was hoping I could just write an event subscriber and bam, be done. But, that's not what happened and why I am writing this blog.

Why the difference? When we first began reviewing our workflow requirements, I remember Bojan sat with the Workbench and Content Moderation folks to try and harmonize efforts. Unfortunately we couldn't reconcile a few differences, and we went with creating an 8.x branch for State Machine and Drupal core went on its own path.

First I wanted to find out what the Workflows module does whenever there is a state change in a workflow due to a transition. The first thing I hopefully looked for was an Events namespace or at least a legacy *.api.php file. Nothing. I found \Drupal\workflows\Transition, but it is just a value object. Turns out the Workflows module does not provide much beyond a plugin type for defining a WorkflowType and several value objects. All of the real work is inside of the Content Moderation module.

The Content Moderation module adds a computed field to entities which has content moderation

/**
 * Implements hook_entity_base_field_info().
 */
function content_moderation_entity_base_field_info(EntityTypeInterface $entity_type) {
  return \Drupal::service('class_resolver')
    ->getInstanceFromDefinition(EntityTypeInfo::class)
    ->entityBaseFieldInfo($entity_type);
}

This adds a moderation_state computed field using the \Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList class. Moderation state for entities are stored as content_moderation_state entities. My hope was that the field item list or this moderation state entity would provide some ways to hook into a transition. Nothing that I could see, at least that isn't baked into the entity form structure.

I finally found the magic which publishes an entity: \Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList::updateModeratedEntity. In this method is toggles the published state:

      // Update publishing status if it can be updated and if it needs updating.
      $published_state = $current_state->isPublishedState();
      if (($entity instanceof EntityPublishedInterface) && $entity->isPublished() !== $published_state) {
        $published_state ? $entity->setPublished() : $entity->setUnpublished();
      }

So, there doesn't seem to be much of an API available. I resorted to the brute force method of using Drupal's hook system in hook_ENTITY_TYPE_presave. The final code involves fetching the original node and comparing if the node has become published.

/**
 * Implements hook_ENTITY_TYPE_presave().
 */
function glamanate_posse_node_presave(\Drupal\node\NodeInterface $node) {
  if (isset($node->original) && !$node->isNew()) {
    $original = $node->original;
    assert($original instanceof NodeInterface);
    if (!$original->isPublished() && $node->isPublished()) {
      $time = \Drupal::getContainer()->get('datetime.time');
      assert($time !== NULL);
      $node->setCreatedTime($time->getRequestTime());
    }
  }
}

I was really hoping to avoid going back into the Drupal 7 days of writing code, but. It gets the job done.

Here's the following test that I wrote for myself:

  public function testChangeCreatedDate() {
    $time = $this->container->get('datetime.time');

    $test_time = $time->getRequestTime() - 1000;
    $node = Node::create([
      'type' => 'page',
      'title' => 'Test page',
      'created' => $test_time,
      'status' => FALSE,
    ]);
    $node->save();
    $node = $this->reloadEntity($node);
    assert($node instanceof NodeInterface);
    $this->assertEquals($test_time, $node->getCreatedTime());

    $node->setPublished();
    $node->save();
    $node = $this->reloadEntity($node);
    assert($node instanceof NodeInterface);
    $this->assertEquals($time->getRequestTime(), $node->getCreatedTime());
  }

🤷‍♂️it works.

I don't use Content Moderation outside my personal site, much, but I'm tempted to write a content_moderation_events module that dispatches events. Unfortunately there's a handle of concrete references to the ModerationStateFieldItemList class.

#