Problem / Motivation

Currently, Workflow API exposes a bunch of hooks for interacting with Workflows. Hooks are procedural by nature, and leave a lot to be desired, especially with complex cases. As I understand it, Drupal 9 will be eliminating hooks, possibly in favor of HookEvent. Even if all hooks aren't deprecated, implementing Events should provide some advantages for other modules to subscribe to Workflow events and obtain relevant information ( such as $entity, $wid, $from_sid, $to_sid, etc ).

Proposed resolution

Register Events for the various Workflow transitions, dispatch the events when the transitions occur. The existing hooks could remain side-by-side with the new events to prevent a breaking change.

Remaining tasks

Define the events and create a dispatcher.

API changes

For D8, all the hooks would stay in place, the event system would basically be an additional layer.

Data model changes

I don't think any data model changes would be necessary to add an event system. I could be wrong, I am just getting into the event system :)

Comments

scottsawyer created an issue. See original summary.

johnv’s picture

"Drupal 9 will be eliminating hooks, possibly in favor of HookEvent."

Can you share some references to this change?

Thanks.

scottsawyer’s picture

@johnv, sorry for the slow response, I somehow missed the notification that you responded.

My language was too precise, I don't think there are final decisions, but discussions seemingly point that direction.

A few references:

While none of these posts are definitive, in my interpretation, hooks may be on the way out, and implementing Events in Workflow could offer advantages.

For my part, I am still learning the Event system, so I am not really ready to submit a patch.

sagesolutions’s picture

Status: Active » Needs review
StatusFileSize
new19.85 KB

Hello!

I've created a patch that dispatches events upon workflow transitions.

I heavily used the state_machine module as a reference.

In your event subscriber, you can do the following as an example

   /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events = [
      'workflow.post_transition' => 'onGenericPostTransition',
    ];
    return $events;

...

  public function onGenericPostTransition(WorkflowTransitionEvent $event) {
     /**
     * @var Node $node
     */
    $node = $event->getEntity();
     $this->logger->notice("Node # " . $node->id() . " onGenericPostTransition event triggered.");
  }

It triggers on pre and post save actions, both of which you can target. For example: 'my_custom_workflow.my_custom_transition.pre_transition'.

sagesolutions’s picture

StatusFileSize
new19.65 KB
johnv’s picture

Title: Emit events that can be subscribed to. » Trigger events that can be subscribed to (D9)

Marking this as a D9 feature.

sagesolutions’s picture

StatusFileSize
new5.72 KB

New simplified patch to work on latest dev

sagesolutions’s picture

Status: Needs review » Needs work

The last submitted patch, 7: 3038853_7.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

johnv’s picture

Generally, I do like new features, and usage of existing architecture.
In this module, I removed some custom hooks, in favour of hook_entity_* hooks.

Please see below code from EntityStrageBase.php, that calls the pre- and post- hooks.
I was looking into that code, hoping to find general events from core.

  /**
   * Performs presave entity processing.
   */
  protected function doPreSave(EntityInterface $entity) {
    $id = $entity->id();
...
    // Allow code to run before saving.
    $entity->preSave($this);
    $this->invokeHook('presave', $entity);
...
  }

  /**
   * Performs storage-specific saving of the entity.
   */
  abstract protected function doSave($id, EntityInterface $entity);

  /**
   * Performs post save entity processing.
   */
  protected function doPostSave(EntityInterface $entity, $update) {
...
    // Allow code to run after saving.
    $entity->postSave($this, $update);
    $this->invokeHook($update ? 'update' : 'insert', $entity);
...
  }

We can do add example code in workflow.api.php, adding the event call in the hook :
- hook_ENTITY_TYPE_presave()
- hook_entity_presave()
postSave():
- hook_ENTITY_TYPE_insert()
- hook_ENTITY_TYPE_update()
The hook contains the Transitions entity, allowing all data to be fetched.

Your event adds get-fucntions for all attributes of the Transition.
Even though this is more explicit, isn't it sufficient to only add the Transition entity? It contains all requested data.

johnv’s picture

I will try to create a patch with new hooks for workflow_transition CRUD.

ayalon’s picture

It is very sad, that patch #7 still is not commited. Its such a superb feature for all developers. The patch does not break any existing functionalities, it just add support for event subscribers.

We are using it on productions since month and it works flawlessly. Please consider merging this patch in the next release.

ayalon’s picture

Status: Needs work » Reviewed & tested by the community
rudi teschner’s picture

I think the patch in #7 should be updated and i.e. include event classes so that handling of events and subscriptions is consistent across modules.

Also, it does not seem to work for D10 since the event dispatcher changed.

rudi teschner’s picture

Here is a version that works for me on a D10 website with D10.1.4 and workflow 8.x-1.7

Along with some exemplary code on how to use them in a custom module:

class WorkflowTransitionSubscriber implements EventSubscriberInterface {

  /**
   * Constructor
   */
  public function __construct(){

  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    $events = [];
    $events[WorkflowEvents::PRE_TRANSITION][] = ['preTransition'];
    $events[WorkflowEvents::POST_TRANSITION][] = ['postTransition'];

    return $events;
  }

  public function preTransition(WorkflowPreTransitionEvent $event) {
    //ksm("preTransition", $event);
  }

  public function postTransition(WorkflowPostTransitionEvent $event) {
    //ksm("postTransition", $event);
  }

yassersamman’s picture

Status: Reviewed & tested by the community » Needs review
ayalon’s picture

I have tested #10 and using the patch. It works well with Drupal 10. Using this patch since ages.

ayalon’s picture

StatusFileSize
new2.57 KB

Here is an updated patch following the event naming from Patch #7.

I prefer this approach as it is more flexible.

ayalon’s picture

StatusFileSize
new5.08 KB

For some reasons the patch did not properly export. Please use this file:

  • johnv committed aaef3b4a on 8.x-1.x authored by Rudi Teschner
    Issue #3038853 by sagesolutions, Rudi Teschner: Trigger events that can...
johnv’s picture

Version: 8.x-1.x-dev » 8.x-1.7
Status: Needs review » Fixed

Thanks, I committed a patch with some changes.

Only 1 TransactionEvent type was committed. The Event interface allows to only use the $event_name. So adding multiple subevents does not have added value.
All functions to read attributes of the $transition are not committed, while redundant. You can to $event->getTransition()->getFromSid().
For that, i assured the WorkflowTransitionInterface was completed here: #3418871: Improve WorkflowTransitionInterface
To make the impact of the commit smaller, the following was needed: #3418870: Restructure save() method, avoiding duplicate inserts
This way, the dispatchEvent() function can be inside, not around the save() function. This way, the events are also triggered for ScheduledTransitions.

I hope these changes do not ruin you project code too much.

  • johnv committed 378c2651 on 8.x-1.x authored by Rudi Teschner
    Issue #3038853 by sagesolutions, Rudi Teschner: Trigger events that can...

  • johnv committed 6280e9ce on 8.x-1.x authored by Rudi Teschner
    Issue #3038853 by sagesolutions, Rudi Teschner: Trigger events that can...
johnv’s picture

The EventSubscriber from #16 is added with dummy code.

sagesolutions’s picture

@johnv

Was it intentional to remove the entity and workflow from the WorkflowTransitionEvent?

In my eventsubscriber, I used the $event->getEntity() method to get the node.

+
+  /**
+     * The workflow.
+     *
+     * @var \Drupal\workflow\Entity\WorkflowInterface
+     */
+  protected $workflow;
+
+  /**
+     * The entity.
+     *
+     * @var \Drupal\Core\Entity\ContentEntityInterface
+     */
+  protected $entity;
...
+  public function __construct(WorkflowTransition $transition, WorkflowInterface $workflow, EntityInterface $entity) {
+        $this->transition = $transition;
+        $this->workflow = $workflow;
+        $this->entity = $entity;
+      }
+
+
+  /**
+     * Gets the workflow.
+     *
+     * @return \Drupal\workflow\Entity\WorkflowInterface
+     *   The workflow.
+     */
+  public function getWorkflow() {
+        return $this->workflow;
+  }
+
+  /**
+     * Gets the entity.
+     *
+     * @return \Drupal\Core\Entity\EntityInterface
+     *   The entity.
+     */
+  public function getEntity() {
+        return $this->entity;
+  }

Also, I used a specific event to trigger my subscribed events. For example 'permit_status_workflow.permit_status_workflow_open.post_transition' => 'onOpenTransition',

Is this still possible? I think this functionality has been removed??

+  /**
+   * Dispatches a transition event for the given phase.
+   *
+   * @param string $phase
+   *   The phase: pre_transition OR post_transition.
+   */
+  protected function dispatchTransitionEvent($phase) {
+
+    $workflow = $this->getWorkflow();
+
+    $event = new WorkflowTransitionEvent($this, $workflow, $this->getTargetEntity());
+    $event_dispatcher = \Drupal::getContainer()->get('event_dispatcher');
+
+    $events = [
+      // For example: 'my_custom_workflow.my_custom_transition.pre_transition'.
+      $this->getWorkflowId() . '.' . $this->getToSid() . '.' . $phase,
+      // For example: 'my_custom_workflow.pre_transition'.
+      $this->getWorkflowId() . '.' . $phase,
+      // For example: 'workflow.pre_transition'.
+      'workflow.' . $phase,
+    ];
+
+    foreach ($events as $event_id) {
+      $event_dispatcher->dispatch($event, $event_id);
+    }
+  }
+
sagesolutions’s picture

Status: Fixed » Needs review
johnv’s picture

For the first block, you can make the following modifications. They will work anyway. As stated, you can get the info using the TransitionInterface:
perhaps changing the constructor to
public function __construct(WorkflowTransition $transition, WorkflowInterface $workflow = NULL, EntityInterface $entity = NULL) {}
or
public function __construct(WorkflowTransition $transition) {}

+
+  /**
+     * The workflow.
+     *
+     * @var \Drupal\workflow\Entity\WorkflowInterface
+     */
+  // protected $workflow;
+
+  /**
+     * The entity.
+     *
+     * @var \Drupal\Core\Entity\ContentEntityInterface
+     */
+  //  protected $entity;
...
+  public function __construct(WorkflowTransition $transition, WorkflowInterface $workflow, EntityInterface $entity) {
+        $this->transition = $transition;
+ //       $this->workflow = $workflow;
+ //       $this->entity = $entity;
+      }
+
...
+  public function getWorkflow() {
+        return $this->transition->getWorkflow();  // CHANGED
+  }
+
+  public function getEntity() {
+        return $this->transition->getTargetEntity();  // CHANGED
+  }

  • johnv committed 1b4f1161 on 8.x-1.x
    Issue #3038853: Remove obsolete WorkflowStorage~getRegisteredEvents()
    
johnv’s picture

@sagesolutions,
for your second block, yes, you still can do what you need to do. Please see below code suggestion:

+  /**
+   * Dispatches a transition event for the given phase.
+   *
+   * @param string $phase
+   *   The phase: pre_transition OR post_transition.
+   */
+  protected function dispatchTransitionEvent($phase) {
+
+    $events = [
+      // For example: 'my_custom_workflow.my_custom_transition.pre_transition'.
+      $this->getWorkflowId() . '.' . $this->getToSid() . '.' . $phase,
+      // For example: 'my_custom_workflow.pre_transition'.
+      $this->getWorkflowId() . '.' . $phase,
+      // For example: 'workflow.pre_transition'.
+      'workflow.' . $phase,
+    ];
+
+    $transition = $this->getTransition();
+    foreach ($events as $event_id) {
+      $transition->dispatchEvent( $event_id);
+    }
+  }
+
sagesolutions’s picture

Hi @johnv,
I like the getEntity() and getWorkflow() methods in the WorkflowTransistionEvent class. I can work with that. Please commit these updates :)

+  public function getWorkflow() {
+        return $this->transition->getWorkflow();  // CHANGED
+  }
+
+  public function getEntity() {
+        return $this->transition->getTargetEntity();  // CHANGED
+  }

For the subscribed events, I liked the ability of what was there before. Using the original patch, I can subscribe to each individual workflow change. For example:

  public static function getSubscribedEvents(): array {
    return [
      'permit_status_workflow.permit_status_workflow_creation.post_transition' => 'onCreationTransition',
      'permit_status_workflow.permit_status_workflow_draft.post_transition' => 'onDraftTransition',
      'permit_status_workflow.permit_status_workflow_open.post_transition' => 'onOpenTransition',
      'permit_status_workflow.permit_status_workflow_pending.post_transition' => 'onPendingTransition',
      'permit_status_workflow.permit_status_workflow_approved.post_transition' => 'onApprovedTransition',
      'permit_status_workflow.permit_status_workflow_declined.post_transition' => 'onDeclinedTransition',
    ];
  }

But now, I can only subscribe to the one post transition event. Then in the called function, I need to check the transition to/from and break apart from there. This seems a bit messier than before.

sagesolutions’s picture

Status: Needs review » Reviewed & tested by the community

I've managed to update my EventSubscriber to work with the new workflow updates. Below is what works for me, maybe it will help others while upgrading.

/**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    return [
      'workflow.post_transition' => 'onWorkflowPostTransition',
    ];
  }

  public function onWorkflowPostTransition(WorkflowTransitionEvent $event): void {
    switch ($event->getTransition()->getToSid()) {
      case "permit_status_workflow_pending" :
        $this->onPendingTransition($event);
        break;
      case "permit_status_workflow_open":
        $this->onOpenTransition($event);
        break;
      case "permit_status_workflow_approved":
        $this->onApprovedTransition($event);
        break;
      case "permit_status_workflow_declined":
        $this->onDeclinedTransition($event);
        break;
    }
  }

  • johnv committed 4f2e6552 on 8.x-1.x authored by sagesolutions
    Issue #3038853 by sagesolutions: Trigger events that can be subscribed...
johnv’s picture

Status: Reviewed & tested by the community » Fixed

Thanks, I added the code to the EventSubscriber.

  • johnv committed c3f2a833 on 8.x-1.x
    Issue #3038853: Trigger events that can be subscribed to - example code
    
johnv’s picture

Some example code is added to the workflow_devel module.

  • johnv committed 60a07dc9 on 8.x-1.x
    Issue #3038853: Trigger events that can be subscribed to - example code
    

  • johnv committed d0c66941 on 8.x-1.x
    Issue #3038853: Trigger events that can be subscribed to - example code
    

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

johnv’s picture