Mettre en place des processus métier complexes avec State machine sur Drupal 8

touches veille machine

Nous avons vu dans un précédent billet comment mettre en place un processus de publication sur Drupal 8 avec les modules Content moderation et Workflows. Nous allons aborder ici une problématique similaire mais en s'appuyant cette fois sur le module State machine, module qui va nous permettre de mettre en place un ou plusieurs processus métier sur n'importe quelle entité de Drupal. A noter que le module state machine est une des composantes essentielles de Drupal Commerce 2.x.

Le fonctionnement du module est assez simple dans sa conception :

  • Les workflows doivent être définis dans un module personnalisé au moyen d'un fichier YAML : ces workflows contiennent à la fois les différents états possibles et les différentes transitions possibles entre chaque état
  • Le module met à disposition un nouveau type de champ : State
  • Nous pouvons ajouter sur une entité autant de champs de type State que nécessaire, et nous pouvons configurer quel processus chaque champ utilisera
  • Et désormais à chaque modification de statut d'un champ State, un événement sera propagé (cf. Drupal 8 et les événements), et nous pourrons alors agir sur ces événements et déclencher autant d'actions que nécessaires

Il restera ainsi à implémenter certaines logiques pour que les champs ainsi créés remplissent au mieux leur rôle.

Notamment, nous devrons générer les différentes permissions nous permettant d'affecter des droits à chaque transition (ou adopter une toute autre logique en ce concerne la gestion des droits, qui pourrait par exemple être basée sur certains attributs des utilisateurs) et encore déclencher les actions adéquates en fonction de chaque statut actif pour un contenu ou n'importe quelle entité.

A noter que vous pourriez avoir besoin d'appliquer ce patch pour disposer d'un événement générique propagé par le champ State machine. Dans le cas contraire, vous pouvez toujours simplement utiliser les événements standard propagés, événements basés sur l'identifiant de la transition déclenchée.

Découvrons plus en détails le fonctionnement de state machine.

Création des processus

A l'inverse des modules Content moderation et Workflows intégrés désormais dans le coeur de Drupal, où nous pouvons tout réaliser en quelques clics, la configuration de processus métier avec State machine nécessite la création d'un module personnalisé pour déclarer les processus et une petite partie de Site building que nous aborderons par la suite brièvement. 

Nous allons créer un nouveau module que nous allons intituler my_workflow. Ce module aura cette structure de base.

├── composer.json
├── my_workflow.info.yml
├── my_workflow.module
├── my_workflow.permissions.yml
├── my_workflow.services.yml
├── my_workflow.workflow_groups.yml
├── my_workflow.workflows.yml
├── src
│   ├── EventSubscriber
│   │   └── WorkflowTransitionEventSubscriber.php
│   ├── Guard
│   │   └── PublicationGuard.php
│   ├── MyWorkflowPermissions.php
│   ├── Tests
│   │   └── LoadTest.php
│   ├── WorkflowHelper.php
│   ├── WorkflowHelperInterface.php
│   └── WorkflowUserProvider.php
└── templates
    └── my_workflow.html.twig

 

Les éléments les plus importants qui vont nous intéresser ici seront les fichiers

  • my_workflow.workflow_groups.yml qui va nous permettre de créer des groupes de processus
  • my_workflow.workflows.yml qui va nous permettre de déclarer et décrire précisément chaque processus
  • PublicationGuard.php qui va nous permettre de gérer les droits d'accès à chaque transition
  • Et enfin WorkflowtransitionEventSubscriber.php qui va nous permettre de réagir en fonction des différents statuts et transitions de chaque processus

Un processus State machine doit obligatoirement appartenir à un groupe de processus. Nous allons donc créer deux groupes de processus (publication et technic) pour notre exemple en les déclarant simplement dans le fichier my_workflow.workflow_groups.yml.

publication:
  label: Publication
  entity_type: node
technic:
  label: Technic
  entity_type: node

Nous pouvons maintenant déclarer nos deux processus métier que nous allons appeler Publication status et Technical status dans le fichier my_workflow.workflows.yml.

publication_default:
  id: publication_default
  label: Default publication
  group: publication
  states:
    new:
      label: New
    draft:
      label: Draft
    proposed:
      label: Proposed
    validated:
      label: Validated
    needs_update:
      label: Needs update
    archived:
      label: Archived
  transitions:
    save_as_draft:
      label: Save as draft
      from: [new, draft, needs_update]
      to: draft
    save_new_draft:
      label: Save new draft
      from: [validated]
      to: draft
    propose:
      label: Propose
      from: [new, draft, needs_update]
      to: proposed
    update_proposed:
      label: Update
      from: [proposed]
      to: proposed
    validate:
      label: Publish
      from: [new, draft, proposed]
      to: validated
    update_validated:
      label: Update
      from: [validated]
      to: validated
    needs_update:
      label: Request changes
      from: [proposed, validated]
      to: needs_update
    unarchive:
      label: Unarchive
      from: [archived]
      to: draft
    archive:
      label: Archive
      from: [validated]
      to: archived

technic_default:
  id: technic_default
  label: Default technic
  group: technic
  states:
    new:
      label: New
    draft:
      label: Draft
    need_review:
      label: Need review
    validated:
      label: Validated
    need_update:
      label: Need update
  transitions:
    save_as_draft:
      label: Save as draft
      from: [new, draft, need_review, validated, need_update]
      to: draft
    ask_review:
      label: Ask review
      from: [new, draft, need_update]
      to: need_review
    update_need_review:
      label: Update review
      from: [need_review]
      to: need_review
    update_need_update:
      label: Stay in need update
      from: [need_update]
      to: need_update
    validate:
      label: Validate
      from: [new, draft, need_review, need_update]
      to: validated
    update_validated:
      label: Update validated
      from: [validated]
      to: validated
    need_update:
      label: Request changes
      from: [need_review, validated]
      to: need_update

Pour chacun des processus nous allons déclarer les différents états du processus (sous la clé states) ainsi que les différentes transitions possibles entre ces statuts (sous la clé transitions). Nous pourrons contrôler par la suite très finement les différents droits pour accéder à ces transitions.

Nous disposons avec le fichier ci-dessus d'un processus standard, classique, et attendu par le module State machine. Mais comme il s'agit de la configuration manuelle (au moyen d'un fichier YAML) des Plugins de Workflow du module State Machine, rien ne nous interdit ici de rajouter des propriétés arbitraires qui vont rendre notre processus métier encore plus intelligent. Nous n'aurons plus qu'à détecter la présence de ces propriétés arbitraires et leur valeur lors de la propagation des événements de State machine. Par exemple, nous allons associer à certains statuts plusieurs propriétés qui pourront nous être utiles dans notre processus :

  • published: true pour publier automatiquement le contenu
  • archived: true pour dépublier automatiquement le contenu
  • notify_owner: true pour notifier automatiquement l'auteur du contenu
  • notify_role: editor pour notifier automatiquement les utilisateurs ayant le role editor par exemple
  • notify_users: field_ref_users pour notifier des utilisateurs référencés depuis un champ particulier (ici le champ field_ref_users)
  • etc.
  • etc.

Nous pourrions tout aussi bien ajouter la propriété create_task: my_task pour par exemple créer une tâche quelconque dans un système tiers, ou encore créer une entité Task sur le site Drupal 8. Mais je crois que vous avez compris le principe : on peut tout faire selon nos besoins.

Ainsi par exemple, nos processus vont désormais ressembler plutôt à cela, avec ces nouvelles propriétés que nous avons ajoutées pour les besoins des processus métier que nous voulons implémenter.

publication_default:
  id: publication_default
  label: Default publication
  group: publication
  states:
    new:
      label: New
    draft:
      label: Draft
    proposed:
      label: Proposed
    validated:
      label: Validated
      published: true
    needs_update:
      label: Needs update
    archived:
      label: Archived
      archived: true
  transitions:
    save_as_draft:
      label: Save as draft
      from: [new, draft, needs_update]
      to: draft
    save_new_draft:
      label: Save new draft
      from: [validated]
      to: draft
    propose:
      label: Propose
      from: [new, draft, needs_update]
      to: proposed
    update_proposed:
      label: Update
      from: [proposed]
      to: proposed
    validate:
      label: Publish
      from: [new, draft, proposed]
      to: validated
    update_validated:
      label: Update
      from: [validated]
      to: validated
    needs_update:
      label: Request changes
      from: [proposed, validated]
      to: needs_update
    unarchive:
      label: Unarchive
      from: [archived]
      to: draft
    archive:
      label: Archive
      from: [validated]
      to: archived

technic_default:
  id: technic_default
  label: Default technic
  group: technic
  states:
    new:
      label: New
    draft:
      label: Draft
    need_review:
      label: Need review
      notify_role: technic
      notify_users: field_notify_users
    validated:
      label: Validated
      notify_owner: true
    need_update:
      label: Need update
      notify_owner: true
  transitions:
    save_as_draft:
      label: Save as draft
      from: [new, draft, need_review, validated, need_update]
      to: draft
    ask_review:
      label: Ask review
      from: [new, draft, need_update]
      to: need_review
    update_need_review:
      label: Update review
      from: [need_review]
      to: need_review
    update_need_update:
      label: Stay in need update
      from: [need_update]
      to: need_update
    validate:
      label: Validate
      from: [new, draft, need_review, need_update]
      to: validated
    update_validated:
      label: Update validated
      from: [validated]
      to: validated
    need_update:
      label: Request changes
      from: [need_review, validated]
      to: need_update

 

Nous pouvons désormais activer le module, et configurer les champs State machine.

Configuration initiale

Il suffit d'ajouter un champ State sur le type d'entité pour le quel nous souhaitons mettre en place un processus. Ici nous allons créer un champ State machine (Publication status) sur le type de contenu Article.

add state machine field

 

Et nous configurons ce champ pour utiliser un des processus que nous allons déclarer dans notre module (cf. plus loin)

Configuration state machine

Nous pouvons configurer le mode d'affichage du formulaire pour positionner le champ State machine, et dans l'exemple ci-dessous, nous en profitons aussi pour désactiver le champ natif Published des types de contenus, qui ne nous servira plus directement.

configuration form mode

Nous pouvons aussi configurer différents modes d'affichage pour le champ State machine en affichant la valeur du statut actuel (ici pour un le champ Publication status) ou encore en en choisissant d'afficher un formulaire qui donne accès aux différentes transitions possibles (ici pour un autre champ que nous avons créé également : Technical status) directement depuis la page de consultation du contenu.

State machine view mode example

Gestion des droits d'accès aux transitions possibles

Par défaut, State machine ne propose aucun type de permissions pour les transitions. Par défaut toutes les transitions sont accessibles aux utilisateurs pouvant éditer un champ State machine.

Pour contrôler l'accès aux transitions il suffit de créer un (ou plusieurs) service qui implémentera GuardInterface. Et c'est au moyen de ce service que nous pourrons simplement interdire l'accès aux transitions, selon notre logique. Pour l'exemple, nous allons prendre dans un premier temps un cas simple et générer autant de permissions qu'il y a de transitions possibles. Le but est de proposer depuis l'interface de gestion des permissions des cases à cocher permettant d'attribuer à des rôles d'utilisateur l'accès à certaines transitions.

Créons le fichier my_workfloww.permissions.yml, avec le contenu suivant

permission_callbacks:
  - \Drupal\my_workflow\MyWorkflowPermissions::permissions

Créons la classe MyWorkPermissions

<?php

namespace Drupal\my_workflow;

use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\core\Entity\EntityTypeManagerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\state_machine\WorkflowManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;


/**
 * Defines a class for dynamic permissions based on workflows.
 */
class MyWorkflowPermissions implements ContainerInjectionInterface {

  use StringTranslationTrait;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The workflow manager.
   *
   * @var \Drupal\state_machine\WorkflowManagerInterface
   */
  protected $workflowManager;


  /**
   * Constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkflowManagerInterface $workflow_manager) {
    $this->entityTypeManager = $entity_type_manager;
    $this->workflowManager = $workflow_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity_type.manager'),
      $container->get('plugin.manager.workflow')
    );
  }

  /**
   * Returns an array of transition permissions.
   *
   * @return array
   *   The transition permissions.
   */
  public function permissions() {
    $permissions = [];
    $workflows = $this->workflowManager->getDefinitions();
    /* @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */
    foreach ($workflows as $workflow_id => $workflow) {
      foreach ($workflow['transitions'] as $transition_id => $transition) {
        $permissions['use ' . $transition_id . ' transition in ' . $workflow_id] = [
          'title' => $this->t('Use the %label transition', [
            '%label' => $transition['label'],
          ]),
          'description' => $this->t('Workflow group %label', [
            '%label' => $workflow['label'],
          ]),
        ];
      }
    }
    return $permissions;
  }

}

Vidons les caches, et nous obtenons notre belle interface de gestion des droits

Permissions state machine

 

Il ne reste plus qu'à créer notre service qui va contrôler l'accès aux transitions sur la base de ces permissions.

Créons le fichier my_workflow.services.yml et déclarons notre service my_workflow.publication_guard

services:
  my_workflow.publication_guard:
    class: Drupal\my_workflow\Guard\PublicationGuard
    arguments: ['@current_user', '@plugin.manager.workflow']
    tags:
      - { name: state_machine.guard, group: publication }
  my_workflow.workflow.helper:
    class: Drupal\my_workflow\WorkflowHelper
    arguments: ['@current_user']
  my_workflow.workflow_transition:
    class: Drupal\my_workflow\EventSubscriber\WorkflowTransitionEventSubscriber
    arguments: ['@my_workflow.workflow.helper']
    tags:
      - { name: event_subscriber }

Notre fichier contient également quelques autres services sur lesquels nous aurons l'occasion de revenir. A noter que votre service doit être affecté à un groupe de processus au moyen de la clé group. Et nous le tagguons avec le tag state_machine.guard pour que ce service soit collecté et évalué par le module state machine.

Créons notre classe PublicationGuard

<?php

/**
 * @file
 * Contains \Drupal\my_workflow\Guard\PublicationGuard.
 */

namespace Drupal\my_workflow\Guard;

use Drupal\Core\Session\AccountProxyInterface;
use Drupal\state_machine\Guard\GuardInterface;
use Drupal\state_machine\Plugin\Workflow\WorkflowInterface;
use Drupal\state_machine\Plugin\Workflow\WorkflowTransition;
use Drupal\Core\Entity\EntityInterface;
use Drupal\state_machine\WorkflowManagerInterface;

class PublicationGuard implements GuardInterface {

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountProxyInterface
   */
  protected $currentUser;

  /**
   * The workflow manager.
   *
   * @var \Drupal\state_machine\WorkflowManagerInterface
   */
  protected $workflowManager;

  /**
   * Constructs a new PublicationGuard object.
   *
   * @param \Drupal\Core\Session\AccountProxyInterface $current_user
   *   The current user..
   */
  public function __construct(AccountProxyInterface $current_user, WorkflowManagerInterface $workflow_manager) {
    $this->currentUser = $current_user;
    $this->workflowManager = $workflow_manager;
  }

  /**
   * {@inheritdoc}
   */
  public function allowed(WorkflowTransition $transition, WorkflowInterface $workflow, EntityInterface $entity) {
    // Don't allow transition for users without permissions.
    $transition_id = $transition->getId();
    $workflow_id = $workflow->getId();
    if (!$this->currentUser->hasPermission('use ' . $transition_id . ' transition in ' . $workflow_id)) {
      return FALSE;
    }
  }

}

La principale (et unique) méthode à utiliser est la méthode allowed() qui doit retourner FALSE quand on souhaite interdire l'accès à une transition sous certaines conditions. Sinon la méthode ne doit rien retourner pour laisser la possibilité aux autres services collectés par la GuardFactory de State machine d'évaluer aussi leur condition. Et si aucun service ne retourne FALSE alors le module State machine donnera accès à la transition.

Dans l'exemple ci-dessus, nous utilisons simplement les permissions précédemment créées pour évaluer si l'utilisateur ne dispose PAS de la permission et dans ce cas retourner FALSE.

Mais nous aurions tout aussi bien pu nous passer de toute cette logique basée sur les permissions standard de Drupal, et évaluer l'accès aux transitions selon des critères beaucoup plus complexes tout en s'évitant une foule de clics sur l'interface de gestion des permissions

Par exemple

/**
 * {@inheritdoc}
 */
public function allowed(WorkflowTransition $transition, WorkflowInterface $workflow, EntityInterface $entity) {
  // Don't allow transition for users without the right attribute.
  if (!$this->userHasComplexAttribute($workflow, $entity)) {
    return FALSE;
  }
}

où la méthode userHasComplexAttribute() peut évaluer ce que la logique métier nécessite (un attribut de l'utilisateur, la valeur d'un champ quelconque, ou les deux en même temps, etc.).

Bref, le champ des possibles est ouvert, à portée de quelques lignes.

Une fois la mise en place des droits d'accès aux transitions réalisée, nous allons pouvoir finaliser nos processus métier en déclenchant les actions requises. 

Mise en place des logiques métier

Une fois les champs créés et liés aux processus, les permissions mises en place, il ne reste plus qu'à utiliser le système des événements de Symfony2, disponibles sur Drupal 8. Chaque champ State machine va propager plusieurs événements lors de chaque transition : un événement pré-transition et un événement post-transition.

Les identifiants de ces événement sont de la forme [group_id].[transition_id].[pre_transition|post_transition], où group_id est l'identifiant du groupe du processus, transition_id est l'identifiant de la transition.

Avec le patch mentionné en introduction (Fire generic events when transition are applied), un événement plus générique sera propagé, dont l'identifiant sera state_machine.[pre_transition|post_transition]. C'est ce dernier événement que nous allons utiliser dans notre exemple.

Pour réagir à ces événements, nous allons donc implémenter un service de type EventSubscriber, que nous retrouvons dans le fichier de déclaration des services de notre module ci-dessous. Nous utiliserons aussi un autre service WorkflowHelper, service qui contiendra quelques méthodes utilitaires. 

services:
  my_workflow.publication_guard:
    class: Drupal\my_workflow\Guard\PublicationGuard
    arguments: ['@current_user', '@plugin.manager.workflow']
    tags:
      - { name: state_machine.guard, group: publication }
  my_workflow.workflow.helper:
    class: Drupal\my_workflow\WorkflowHelper
    arguments: ['@current_user']
  my_workflow.workflow_transition:
    class: Drupal\my_workflow\EventSubscriber\WorkflowTransitionEventSubscriber
    arguments: ['@my_workflow.workflow.helper']
    tags:
      - { name: event_subscriber }

 

Découvrons la classe WorkflowTransitionEventSubscriber qui va nous permettre de réagir selon les différentes transitions.

<?php

namespace Drupal\my_workflow\EventSubscriber;

use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\my_workflow\WorkflowHelperInterface;
use Drupal\state_machine\Event\WorkflowTransitionEvent;
use Drupal\state_machine\Plugin\Workflow\WorkflowInterface;
use Drupal\state_machine\Plugin\Workflow\WorkflowState;
use Drupal\state_machine_workflow\RevisionManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Event subscriber to handle actions on workflow-enabled entities.
 */
class WorkflowTransitionEventSubscriber implements EventSubscriberInterface {

  /**
   * The workflow helper.
   *
   * @var \Drupal\my_workflow\WorkflowHelperInterface
   */
  protected $workflowHelper;

  /**
   * Constructs a new WorkflowTransitionEventSubscriber object.
   *
   * @param \Drupal\my_workflow\WorkflowHelperInterface $workflowHelper
   *   The workflow helper.
   */
  public function __construct(WorkflowHelperInterface $workflowHelper) {
    $this->workflowHelper = $workflowHelper;
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    return [
      'state_machine.pre_transition' => 'handleAction',
    ];
  }

  /**
   * handle action based on the workflow.
   *
   * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event
   *   The state change event.
   */
  public function handleAction(WorkflowTransitionEvent $event) {
    $entity = $event->getEntity();

    // Verify if the new state is marked as published state.
    $is_published_state = $this->isPublishedState($event->getToState(), $event->getWorkflow());

    if ($entity instanceof EntityPublishedInterface) {
      if ($is_published_state) {
        $entity->setPublished();
      }
      else {
        $entity->setUnpublished();
      }

    }
  }

  /**
   * Checks if a state is set as published in a certain workflow.
   *
   * @param \Drupal\state_machine\Plugin\Workflow\WorkflowState $state
   *   The state to check.
   * @param \Drupal\state_machine\Plugin\Workflow\WorkflowInterface $workflow
   *   The workflow the state belongs to.
   *
   * @return bool
   *   TRUE if the state is set as published in the workflow, FALSE otherwise.
   */
  protected function isPublishedState(WorkflowState $state, WorkflowInterface $workflow) {
    return $this->workflowHelper->isWorkflowStatePublished($state->getId(), $workflow);
  }

}

Nous souscrivons ici à l'événement state_machine.pre_transition au moyen de la méthode getSubscribedEvents(), et nous appelons alors la méthode handleAction() qui dans cet exemple simple va nous permettre de publier ou non un contenu en fonction du statut du champ State machine. Pour ce faire nous vérifions nos propriétés personnaliées que nous avons associer aux différents états du processus grâce à la méthode isWorkflowStatePublished() de la classe Utilitaire WorkflowHelper.

Cette dernière méthode va inspecter les paramètres du plugin correspondant au processus en cours, et vérifier si le statut actuel dispose de la propriété published: true.

<?php

namespace Drupal\my_workflow;

use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\state_machine\Plugin\Workflow\WorkflowInterface;
use Drupal\state_machine\Plugin\Workflow\WorkflowTransition;

/**
 * Contains helper methods to retrieve workflow related data from entities.
 */
class WorkflowHelper implements WorkflowHelperInterface {

  /**
   * The current user proxy.
   *
   * @var \Drupal\Core\Session\AccountProxyInterface
   */
  protected $currentUser;

  /**
   * Constructs a WorkflowHelper.
   *
   * @param \Drupal\Core\Session\AccountProxyInterface $currentUser
   *   The service that contains the current user.
   */
  public function __construct(AccountProxyInterface $currentUser) {
    $this->currentUser = $currentUser;
  }


  /**
   * {@inheritdoc}
   */
  public function isWorkflowStatePublished($state_id, WorkflowInterface $workflow) {
    // We rely on being able to inspect the plugin definition. Throw an error if
    // this is not the case.
    if (!$workflow instanceof PluginInspectionInterface) {
      $label = $workflow->getLabel();
      throw new \InvalidArgumentException("The '$label' workflow is not plugin based.");
    }

    // Retrieve the raw plugin definition, as all additional plugin settings
    // are stored there.
    $raw_workflow_definition = $workflow->getPluginDefinition();
    return !empty($raw_workflow_definition['states'][$state_id]['published']);
  }

  /**
   * {@inheritdoc}
   */
  public function isWorkflowStateArchived($state_id, WorkflowInterface $workflow) {
    // We rely on being able to inspect the plugin definition. Throw an error if
    // this is not the case.
    if (!$workflow instanceof PluginInspectionInterface) {
      $label = $workflow->getLabel();
      throw new \InvalidArgumentException("The '$label' workflow is not plugin based.");
    }

    // Retrieve the raw plugin definition, as all additional plugin settings
    // are stored there.
    $raw_workflow_definition = $workflow->getPluginDefinition();
    return !empty($raw_workflow_definition['states'][$state_id]['archived']);
  }

}

Nous pourrions tout aussi facilement vérifier la présence des propriété notify_owner, notify_role, notify_users, etc.

Par exemple, devons-nous notifier certains utilisateurs référencés depuis un certain champ ? Nous récupérerons alors l'identifiant du champ les référençant :

/**
 * {@inheritdoc}
 */
public function isWorkflowStateNotifyUsers($state_id, WorkflowInterface $workflow) {
  // We rely on being able to inspect the plugin definition. Throw an error if
  // this is not the case.
  if (!$workflow instanceof PluginInspectionInterface) {
    $label = $workflow->getLabel();
    throw new \InvalidArgumentException("The '$label' workflow is not plugin based.");
  }

  // Retrieve the raw plugin definition, as all additional plugin settings
  // are stored there.
  $raw_workflow_definition = $workflow->getPluginDefinition();
  $field_name = !empty($raw_workflow_definition['states'][$state_id]['notify_users']) ? $raw_workflow_definition['states'][$state_id]['notify_users'] : FALSE;
  return $field_name;
}

Puis nous pouvons les notifier depuis l'EventSubscriber.

/**
 * handle action based on the workflow.
 *
 * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event
 *   The state change event.
 */
public function handleAction(WorkflowTransitionEvent $event) {
  $entity = $event->getEntity();

  // Verify if we should notify some users.
  $field_name = $this->workflowHelper-> isWorkflowStateNotifyUsers($event->getToState(), $event->getWorkflow());

  if ($entity instanceof FieldableEntityInterface) {
    if ($field_name) {
      $this->notifyUsers($field_name, $event, $entity);
    }

  }
}

En quelques lignes et quelques méthodes, nous pouvons désormais mettre en place toute réaction nécessaire en fonction d'une transition et d'un statut particulier, et ce, rappelons le, sur n'importe quelle entité de Drupal 8.

Content Moderation ou State Machine ?

Quelle solution choisir entre Content moderation et State machine ? Nous avons pu le voir lors de ce long billet de présentation du module State machine, cette solution est avant tout orientée Développeur Drupal 8, à contrario de la solution Content moderation, plus orientée Site builder Drupal 8, qui permet de mettre en place un processus de publication en quelques clics (même si cette solution peut être aussi étendue aussi par des développements supplémentaires).

Chaque solution correspond à un besoin particulier : Content moderation (et le module workflows) répond très rapidement au besoin d'un (et un seul) processus de publication classique sur les contenus (les entités node) d'un site drupal 8, tandis que State machine peut répondre, tout aussi rapidement, à des besoins plus complexes (plusieurs processus sur un même contenu, processus sur n'importe quelle entité de Drupal 8).

Concernant leur support et leur maintenance prévisible, Content moderation en tant que module du coeur de Drupal 8 dispose d'un avantage indéniable. Mais le module State machine n'est pas en reste car il constitue une des briques majeures de Drupal Commerce 2.x. Ces deux solutions sont donc assurées d'être dans le paysage de l'écosystème de Drupal pour un temps certain. Quoiqu'il en soit, cet aspect ne doit pas constituer un élément majeur quant à une décision entre telle ou telle solution. N'hésitez à contacter un expert Drupal 8 qui pourra vous conseiller utilement sur la meilleure stratégie à adopter par rapport à votre projet.   

Si vous le souhaitez, vous pouvez récupérer le code source des exemples présentés dans ce billet sur ce dépôt GitHub.

 

Ajouter un commentaire