Implementing Workflow Component In Symfony With Multiple States Simultaneously

Vandeth Tho
6 min readJan 31, 2024

--

In my previous article, we understood the fundamentals of implementing the Workflow component in Symfony and the use of workflow type state_machine and event handling. Building on that foundation, this article delves deeper into the advanced aspects of workflow management. We’ll explore complex workflows, which, unlike a state_machine, supports multiple states simultaneously, offering a more dynamic and flexible approach.

This blog is the third and final part of our workflow component implementation blogs:

  1. Understanding and Implementing the Workflow Component in Symfony
  2. Implementing and Managing Events in Symfony Workflows
  3. Implementing Workflow Component In Symfony With Multiple States Simultaneously

Workflow: This type is suitable for use cases where an object can be in multiple states (places) at the same time. It is a more complex scenario suitable for non-linear processes. In a Workflow, an object can have multiple paths and can return to previous states.

You can find the full code in this repo: https://github.com/vandetho/blog_app.git

1. Configuration

Let start create our workflow configuration in config/workflows/article_workflow.yaml :

framework:
workflows:
article_workflow:
type: 'workflow'
audit_trail:
enabled: true
marking_store:
type: 'method'
property: 'marking'
supports:
- App\Entity\Article
initial_marking: !php/const App\Workflow\State\ArticleState::NEW_ARTICLE
places:
!php/const App\Workflow\State\ArticleState::NEW_ARTICLE:
!php/const App\Workflow\State\ArticleState::CHECKING_CONTENT:
metadata:
bg_color: ORANGE
!php/const App\Workflow\State\ArticleState::CONTENT_APPROVED:
metadata:
bg_color: DeepSkyBlue
!php/const App\Workflow\State\ArticleState::CHECKING_SPELLING:
metadata:
bg_color: ORANGE
!php/const App\Workflow\State\ArticleState::SPELLING_APPROVED:
metadata:
bg_color: DeepSkyBlue
!php/const App\Workflow\State\ArticleState::PUBLISHED:
metadata:
bg_color: Lime
transitions:
!php/const App\Workflow\Transition\ArticleTransition::CREATE_ARTICLE:
from:
- !php/const App\Workflow\State\ArticleState::NEW_ARTICLE
to:
- !php/const App\Workflow\State\ArticleState::CHECKING_CONTENT
- !php/const App\Workflow\State\ArticleState::CHECKING_SPELLING
!php/const App\Workflow\Transition\ArticleTransition::APPROVE_SPELLING:
from:
- !php/const App\Workflow\State\ArticleState::CHECKING_SPELLING
to:
- !php/const App\Workflow\State\ArticleState::SPELLING_APPROVED
!php/const App\Workflow\Transition\ArticleTransition::APPROVE_CONTENT:
from:
- !php/const App\Workflow\State\ArticleState::CHECKING_CONTENT
to:
- !php/const App\Workflow\State\ArticleState::CONTENT_APPROVED
!php/const App\Workflow\Transition\ArticleTransition::PUBLISH:
from:
- !php/const App\Workflow\State\ArticleState::CONTENT_APPROVED
- !php/const App\Workflow\State\ArticleState::SPELLING_APPROVED
to:
- !php/const App\Workflow\State\ArticleState::PUBLISHED

As you can see, when we create and submit our article, our workflow will transition to 2 states checking_content and checking_spelling, allowing these states to function simultaneously and separately. Furthermore, for the article to be able to publish the previous states need to be at content_approved and spelling_approved at the same time.

As you can see the create 2 PHP files one is for state and another for transition.

So for my state :

<?php
declare(strict_types=1);

namespace App\Workflow\State;

/**
* Class ArticleState
* @package App\Workflow\State
* @author Vandeth THO <thovandeth@gmail.com>
*/
final class ArticleState
{
public const NEW_ARTICLE = 'new_article';
public const CHECKING_CONTENT = 'checking_content';
public const CONTENT_APPROVED = 'content_approved';
public const CHECKING_SPELLING = 'checking_spelling';
public const SPELLING_APPROVED = 'spelling_approved';
public const PUBLISHED = 'published';
}

and for transition:

<?php
declare(strict_types=1);

namespace App\Workflow\Transition;

/**
* Class ArticleTransition
* @package App\Workflow\Transition
* @author Vandeth THO <thovandeth@gmail.com>
*/
final class ArticleTransition
{
public const CREATE_ARTICLE = 'create_article';
public const PUBLISH = 'publish';
public const APPROVE_CONTENT = 'approve_content';
public const APPROVE_SPELLING = 'approve_spelling';
}

2. Adapting Our Entity

Now, we need to create an article entity:

<?php
declare(strict_types=1);

namespace App\Entity;

use App\Repository\ArticleRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

/**
* Class Article
*
* @package App\Entity
* @author Vandeth THO <thovandeth@gmail.com>
*/
#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;

/**
* @var string|null
*/
#[ORM\Column(length: 255, unique: true, nullable: true)]
private ?string $title = null;

/**
* @var string|null
*/
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $content = null;

/**
* @var array
*/
#[ORM\Column(length: 255)]
private array $marking = [];

public function getId(): ?int
{
return $this->id;
}

public function getTitle(): ?string
{
return $this->title;
}

public function setTitle(?string $title): Article
{
$this->title = $title;
return $this;
}

public function getContent(): ?string
{
return $this->content;
}

public function setContent(?string $content): Article
{
$this->content = $content;
return $this;
}

public function getMarking(): array
{
return $this->marking;
}

public function setMarking(array $marking): Article
{
$this->marking = $marking;
return $this;
}
}

3. Visualizing Workflows

Now you can dump the workflow into an image or SVG with Graphviz, provides the dot command:

# using Graphviz's 'dot' and SVG image
php bin/console workflow:dump article_workflow| dot -Tsvg -o workflows/article_workflow.svg

# using Graphviz's 'dot' and PNG image
php bin/console workflow:dump article_workflow| dot -Tpng -o workflows/article_workflow.png

When running the above command you will find the image or SVG of our blog workflow in the workflows/article_workflow.svg or workflows/article_workflow.png.

4. Using the Workflow

After the above instructions, we can now write some logic for the blog workflow:

<?php
declare(strict_types=1);

namespace App\Workflow;

use App\Entity\Article;
use App\Repository\ArticleRepository;
use App\Workflow\Transition\ArticleTransition;
use LogicException;
use Symfony\Component\Workflow\WorkflowInterface;

/**
* Class ArticleWorkflowWorkflow
*
* @package App\Workflow
* @author Vandeth THO <thovandeth@gmail.com>
*/
readonly class ArticleWorkflow
{
/**
* ArticleWorkflowWorkflow constructor.
*
* @param WorkflowInterface $articleWorkflow
* @param ArticleRepository $articleRepository
*/
public function __construct(
private WorkflowInterface $articleWorkflow,
private ArticleRepository $articleRepository,
)
{
}

/**
* Update the article and send it to be reviewed
*
* @param string $title
* @param string $content
* @return Article
*/
public function create(string $title, string $content): Article
{
$article = $this->articleRepository->create();
$article->setTitle($title);
$article->setContent($content);
$this->articleWorkflow->apply($article, ArticleTransition::CREATE_ARTICLE);
$this->articleRepository->save($article);
return $article;
}

/**
* @param int $articleId
* @return Article
*/
public function approveContent(int $articleId): Article
{
$article = $this->getArticle($articleId);
$this->articleWorkflow->apply($article, ArticleTransition::APPROVE_CONTENT);
$this->articleRepository->save($article);
return $article;

}

/**
* @param int $articleId
* @return Article
*/
public function approveSpelling(int $articleId): Article
{
$article = $this->getArticle($articleId);
$this->articleWorkflow->apply($article, ArticleTransition::APPROVE_SPELLING);
$this->articleRepository->save($article);
return $article;

}

/**
* Approve the article and publish it
*
* @param int $articleId
* @return Article
*/
public function publish(int $articleId): Article
{
$article = $this->getArticle($articleId);
$this->articleWorkflow->apply($article, ArticleTransition::PUBLISH);
$this->articleRepository->save($article);
return $article;
}

private function getArticle(int $articleId): Article
{
$article = $this->articleRepository->find($articleId);

if ($article) {
return $article;
}

throw new LogicException('Article not found');
}
}

5. Unit Testing Workflows

Now we can create our testing in tests/Workflow/ArticleWorkflowTest.php:

<?php
declare(strict_types=1);

namespace App\Tests\Workflow;

use App\Workflow\ArticleWorkflow;
use App\Workflow\State\ArticleState;
use Faker\Factory;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

/**
* Class ArticleWorkflowTest
* @package App\Tests\Workflow
*
* @author Vandeth THO <thovandeth@gmail.com>
*/
class ArticleWorkflowTest extends KernelTestCase
{
private ArticleWorkflow $articleWorkflow;

protected function setUp(): void
{
$this->articleWorkflow = self::getContainer()->get(ArticleWorkflow::class);
}

public function testCreateArticle(): int
{
$faker = Factory::create();
$title = $faker->sentence();
$content = $faker->paragraph();
$article = $this->articleWorkflow->create($title, $content);

$this->assertNotNull($article->getId());
$this->assertSame($title, $article->getTitle());
$this->assertSame($content, $article->getContent());
$this->assertArrayHasKey(ArticleState::CHECKING_SPELLING, $article->getMarking());
$this->assertArrayHasKey(ArticleState::CHECKING_CONTENT, $article->getMarking());
return $article->getId();
}

/**
* @depends testCreateArticle
*/
public function testApproveContent(int $articleId): void
{
$article = $this->articleWorkflow->approveContent($articleId);
$this->assertArrayHasKey(ArticleState::CONTENT_APPROVED, $article->getMarking());
$this->assertArrayHasKey(ArticleState::CHECKING_SPELLING, $article->getMarking());
}

/**
* @depends testCreateArticle
*/
public function testApproveSpelling(int $articleId): void
{
$article = $this->articleWorkflow->approveSpelling($articleId);
$this->assertArrayHasKey(ArticleState::CONTENT_APPROVED, $article->getMarking());
$this->assertArrayHasKey(ArticleState::SPELLING_APPROVED, $article->getMarking());
}

/**
* @depends testCreateArticle
*/
public function testPublish(int $articleId): void
{
$article = $this->articleWorkflow->publish($articleId);
$this->assertArrayHasKey(ArticleState::PUBLISHED, $article->getMarking());
}

}

And do not forget to declare our service public in config/services.yaml :

# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.

# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:

services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully qualified class name
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'

# add more service definitions when explicit configuration is needed,
# please note that last definitions always *replace* previous ones
App\Workflow\BlogWorkflow:
public: true
App\Workflow\BlogEventWorkflow:
public: true
App\Workflow\ArticleWorkflow:
public: true

Implementing a multi-state workflow in Symfony is not just about adding complexity; it’s about embracing flexibility and dynamism in your application’s workflow management. Whether you’re building a content management system, an e-commerce platform, or any application requiring intricate workflow processes, Symfony’s Workflow component stands out as a robust solution.

Embrace the power of Symfony and revolutionize the way your application handles complex workflows!

Furthermore, you can simplify and streamline your Symfony Workflow Configuration process by using SymFlowBuilder.

https://www.symflowbuilder.com

--

--

Vandeth Tho

Passionate about technology and innovation, I'm a problem solver. Skilled in coding, design, and collaboration, I strive to make a positive impact.