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:
- Understanding and Implementing the Workflow Component in Symfony
- Implementing and Managing Events in Symfony Workflows
- Simplifying Event Handling in Symfony Workflows with the Event Attribute (Symfony 7.1)
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::PUBLISHEDAs 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: trueImplementing 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.