Gestionando Workflows en Symfony

Gestionando Workflows en Symfony

Hay multitud de situaciones en el desarrollo de proyectos de software en los que hay que gestionar un flujo de trabajo, un proceso, una serie de estados por los que pueda pasar una determinada entidad. Entidad, cómo hablamos, por los que pueda pasar algo que estás modelando vaya.

Un ejemplo fácil sería un pedido en una tienda online, el pedido arranca en estado nuevo y, si se cumplen determinadas condiciones, pasa a pendiente de pago, pagado, enviado, etc.

Un pedido es algo directo, fácil de entender que sigue un flujo de estados, pero realmente hay muchas otras cosas que pueden estar únicamente en una serie de estados y que, de forma natural, podrán moverse o no de un estado a otro. Una tarea en un sistema de gestión de tareas, un curso que alguien quiere hacer, etc.

En Symfony hay un componente específico para la gestión de procesos que se llama Workflow. Puedes añadirlo en tu proyecto con:

composer require symfony/workflow

Los procesos de Symfony se basan en lugares o Places y transiciones entre esos lugares. De la propia web de Symfony, este flujo lo explica bien:

Diagrama que muestra lugares como círculos, transiciones como cuadrados y flechas entre ambos.

Máquinas de estados y workflows

Hay un subtipo de workflow que son las máquinas de estados. La diferencia fundamental es que en una máquina de estados el proceso solo puede estar en un place o estado a la vez. Es decir, usando ejemplos anteriores, un pedido estará únicamente en un estado a la vez (nuevo, pagado, enviado, etc), y solo en un uno. En un workflow genérico, el proceso podría ser multiestado y estar en varios sitios o places a la vez.

En nuestro caso, casi todo lo que hacemos es con máquinas de estado.

Cómo empezar

A nosotros nos gusta pintar el proceso en algún software que ayude a esto, como Whimsical, Figma FigJam o similar.

Además de usarlo para nosotros y como documentación interna, en general es una muy buena herramienta para sentarse con los clientes y ver qué tiene que ocurrir en cada paso, desde qué estado debería poderse ir a qué otro estado, etc. Nos queda algo como la siguiente imagen:

Una vez tenemos ya modelado el proceso, de esta forma o en papel, podemos pasarlo a un fichero .yaml de configuración en el proyecto.

framework:
  workflows:
    article_lifecycle:
      type: 'state_machine'
      marking_store:
        type: 'single_state'
        arguments:
          - 'status'
      supports:
        - App\Entity\Article
      places:
        - draft
        - review
        - published
      transitions:
        to_review:
          from: draft
          to: review
        publish:
          from: review
          to: published
        unpublish:
          from: published
          to: review
      guards:
        to_review:
          - "subject.isEditable() === true"
        publish:
          - "subject.isReviewed() === true"

El anterior es un ejemplo de cómo quedaría un workflow para un Artículo que puede tener estados draft, review y published y cómo serían las transiciones entre unos estados y otros.

Se puede ver que hay 3 places o estados disponibles, draft, review y published.

Podéis ver en transitions que se puede ir de draft a review, de review a published y se puede despublicar también, yendo de published de vuelta a review.

Eventos

Si construyes el Workflow con un EventDispatcher, se lanzarán eventos en un montón de situaciones por las que vaya pasando el Workflow, en concreto:

  • workflow.guard Para validar si la transición está bloqueada o no.
  • workflow.leave Cuando el objeto está a punto de salir de un estado, o place.
  • workflow.transition El objeto está pasando por esta transición.
  • workflow.enter El objeto está entrando en un estado o place.
  • workflow.entered El objeto ya ha entrado en este estado o place.
  • workflow.completed El objeto ya ha completado esta transición.
  • workflow.announce Se lanza para cada transición disponible ahora mismo para el objeto.

Como announce puede lanzar un montón de eventos, es habitual deshabilitarlo.

Aparte, podemos decidir qué eventos lanzar en el yaml (por si solo quieres lanzar un subconjunto de los eventos):

 # you can pass one or more event names
            events_to_dispatch: ['workflow.leave', 'workflow.completed']

De esta forma, con los eventos y escuchando con listeners los que nos interesen, es muy fácil ejecutar en las transiciones lo que sea necesario hacer.

Guards

Hay un tipo de evento especial, y si no es como evento se puede configurar directamente en el yaml, como en el ejemplo de arriba o directamente en cada place, que son los guards:

completed: 
	guard: "subject.hasPaid()"
    	from: [in_progress]
    	to: completed

Los guards permiten definir restricciones para los estados, de tal forma que no se pueda ejecutar una transición o pasar a uno u otro estado, si no se cumplen una serie de condiciones.

Esto hace que, por configuración en el yaml del Workflow, tengamos definido todo nuestro flujo de estados, de qué estado a qué estado se puede pasar, en qué condiciones, qué transición ocurre en ese proceso y qué eventos se lanzan cuando eso ocurre.

La simplicidad a nivel de cantidad de código, facilidad de lectura y robustez, ya que estamos usando un componente de Symfony ya testado para algo que, en muchos casos, puede ser crítico en la plataforma que estemos desarrollando, hacen de los Workflows algo fundamental en muchos desarrollos, al menos para nosotros.

📫
Hasta aquí el artículo de hoy. ¡Si quieres puedes escribirnos por redes sociales como siempre, o a hola@softspring.es con cualquier duda o sugerencia!