El ecosistema de Symfony es muy amplio y cada día crece en número y calidad de sus componentes gracias a su gran comunidad. Lo que nos permite el desacoplamiento de sus componentes, es usarlos sin necesidad de incluir el framework completo. Esto es muy interesante cuando tenemos código legacy o proyectos no realizados con symfony. La principal ventaja de los componentes, son su alta calidad de código, están testados, son frecuentemente actualizados y cuentan con una documentación bastante buena.
En este caso vamos a hacer uso de un componente en particular, llamado workflow. La esencia del componente es sencilla, tener de forma organizada el grafo de estados a los que puede cambiar el estado de la entidad. En caso de hacerlo sobre el framework, nos permite escribir la configuración sobre archivos yaml. Si quieres usarlo de forma independiente, tendrás que realizar tu mismo el paso de lectura y creación del workflow manualmente.
Aspectos clave del componente
Tabla de contenidos
Toggle- Tipología: El componente nos provee dos tipos de máquinas de estado, ‘state_machine’ y ‘workflow’. La diferencia radica en es el estado de la entidad, puesto que en el primero solo acepta 1 estado y en el segundo, puede estar en dos o mas a la vez.
- Sistema de eventos: nos brinda un sistema de eventos, entre los cuales se encuentran: guard, leave, transition, enter, entered, completed, announced. Como se puede observar en la documentación. En el enlace, se muestra 3 niveles de eventos, que van desde el evento para todos los workflows, para uno en concreto y para un workflow en una transición en concreto. Esto nos puede ser muy útil para aplicar acciones a distintos niveles en el proceso de vida del cambio del estado del objeto, siempre para todos los workflows, o únicamente para una transición en un workflow en concreto.
El servicio encargado de aplicar el estado provisto por symfony se llama MethodMarkingStore, y como su propio nombre indica, aplica el estado a la propiedad de un objeto. Esto se puede modificar sin problema, puesto que nos proporcionan una interfaz para realizar la implementación necesaria en caso de necesidad. El siguiente enlace apunta a la parte del framework encargado de cargar la configuración del componente.
Ejemplo de uso
Para nuestro ejemplo, vamos a crear una sencilla entidad que almacene los datos de contacto que un usuario rellena en un formulario web. Para empezar a usarlo, en nuestro caso, vamos a utilizar el skeleton de symfony para que nos facilite el uso del componente:
Ahora instalaremos el componente, puesto que no viene con el skeleton:
Para centrarnos en la temática en cuestión, hemos omitido todas propiedades que almacenan los datos relaventes como el email, el texto de consulta o el id, necesario para la persistencia entre otras, dejando únicamente la propiedad sobre la que vamos a actuar. En caso de que quieras usar los mecanismos de persistencia de doctrine, incluido en el skeleton de symfony, te recomendamos que leas su documentación.
Como habrás podido observar, al setter del estado, le hemos añadido una pequeña comprobación para evitar cualquier valor introducido a la entidad que no esté entre los valores correctos. Esta librería viene incluida al instalar el skeleton de symfony y resulta muy útil para mantener la coherencia de un objeto a la hora de crear o modificar cualquiera de sus propiedades.
Ahora lo que haremos será crear los estados, y las transiciones para que el componente pueda configurar correctamente las transiciones de la entidad:
Para poder usar el componente, hemos creado un sencillo servicio encargado de cambiar los estados de la entidad en función de las acciones que se lleven a cabo desde una interfaz web, por ejemplo.
El servicio únicamente se encarga de modificar el estado en base a la configuración que le hemos proporcionado. En caso de querer persistir la entidad o el estado, lo debes hacer de forma explícita.
Para este servicio, hemos hecho uso del autowiring que nos proporciona symfony, tipando el parámetro en el constructor del servicio. El objeto workflow que nos devuelve, tiene distintos métodos, pero los mas relevantes son apply y can. Con el primero, vamos a ejecutar el flujo encargado de cambiar el estado de la entidad. Con el segundo, consultamos si es posible realizar el cambio desde el estado actual hasta el objetivo.
Al contrario que otras librerías como winzou/state-machine, symfony confía en los eventos para que se ejecuten las acciones necesarias o que bloqueen la transición hacia otros estados con el evento guard.
Como extra, hemos implementado un suscriber del evento guard, para el workflow que hemos estado viendo y la transición mark_as_read, la cual nos sirve para bloquear cambios de estado. Esto lo podemos usar para decidir si bloquear o no la transición en base a otros factores. Incluso, podemos bloquear cualquier cambio de estado, en caso de que detectemos que nuestro servicio esté caído evitando inconsistencia en los datos, como por ejemplo.
Aquí podemos añadir la lógica que necesitemos, puesto que contamos con la entidad almacenada en el evento, y podemos añadirle un contexto en la configuración del workflow.yaml. Si tenemos la entidad en el estado «read», al intentar aplicar la transición «mark_as_spam», obtendremos el siguiente error: Transition «mark_as_spam» is not enabled for workflow «contact_form».» cuando intentamos aplicar la transición para cambiar desde el estado «received» a «spam». Este sería el caso mas específico en el cual necesitamos ejecutar alguna lógica adicional, pero podemos hacer uso en la misma configuración.
Para finalizar, el componente, nos permite crear imágenes de los workflows a través de una herramienta de generación de grafos en este enlace. Esto nos va a ayudar a detectar posibles problemas y visualizar de forma mas nítida el alcance de todas las transiciones de nuestro objeto.
Conclusión
El componente workflow es una forma eficiente y robusta de estipular las transiciones posibles de un objeto. Esto nos evita tener lógica perteneciente a la entidad repartida por diferentes lugares, para conseguir una mayor coherencia. Además, podemos bloquear transiciones así como efectuar acciones en cada paso de la entidad, proporcionándonos una gran versatilidad para controlar el flujo. Esto puede ser una gran idea para objetos que tradicionalmente tienen una lógica compleja, como podría ser los pedidos o los pagos. Es importante siempre usar librerías de terceros que cuenten con test y tengan un repositorio vivo, con PRs, issues y gente dispuesta a seguir manteniendola.