Understanding and Implementing the Symfony Workflow Component in Laravel.

Vandeth Tho
8 min readJan 28, 2024

--

Integrating the Symfony Workflow component into Laravel applications offers developers a powerful tool for managing complex workflows and business processes. The Symfony Workflow component is designed to handle workflows or state machines, enabling a structured approach to managing the life cycle of objects based on defined processes.

In this article, we’ll explore how to implement the Symfony Workflow component in a Laravel application, bringing together Symfony’s robust workflow management capabilities with Laravel’s elegant syntax and features.

Introduction to Symfony Workflow Component

The Symfony Workflow component provides a way to define a workflow or a state machine. A workflow consists of places (states) and transitions (the process of moving from one state to another). This component is particularly useful for applications that require clear management of entity states, such as publishing systems, e-commerce orders, or any domain that involves a sequence of steps or statuses.

The Workflow component in Symfony provides tools for managing a workflow or a series of life-cycle events that an object (like a document or a ticket) goes through.

Key Concepts

  • Places: These are the states or statuses in a workflow.
  • Transitions: Actions that move the subject from one place to another.
  • Workflow: The overall map of places and transitions.

The Symfony Workflow component supports two types of workflows:

  • 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.
  • State Machine: Unlike the Workflow, a State Machine restricts an object to be in only one state (place) at a time. This type is ideal for linear processes or workflows where the progression is straightforward and sequential. In a State Machine, an object moves from one state to another in a defined order, without the possibility of being in multiple states simultaneously or moving backwards.

Today we will start with State Machine. You can use this repo https://github.com/vandetho/laravel_blog_app.

In this article, we will try to build a blog publication workflow like the following:

Installing the Symfony Workflow Component

The first step is to install the Symfony Workflow component in your Laravel application. This is done via Composer:

composer require symfony/workflow

Configuring the Workflow

Once installed, the next step is to define your workflows. In Laravel, this is done by creating a configuration file, typically named workflow.php, in the config directory. Here, you can outline the structure of your workflow, including its states (places) and transitions.

Example config/workflow.php:

<?php

use App\Workflow\State\BlogState;
use App\Workflow\Transition\BlogTransition;

return [
'blog_publishing' => [
'type' => 'state_machine',
'audit_trail' => ['enabled' => true],
'marking_store' => [
'type' => 'method',
'property' => 'state'
],
'supports' => [App\Models\Blog::class],
'places' => [
BlogState::NEW_BLOG,
[BlogState::NEED_REVIEW => ['metadata' => ['bg_color' => 'DeepSkyBlue']]],
[BlogState::NEED_UPDATE => ['metadata' => ['bg_color' => 'Orchid']]],
[BlogState::PUBLISHED => ['metadata' => ['bg_color' => 'Lime']]],
],
'initial_marking' => 'BlogState::NEW_BLOG',
'transitions' => [
BlogTransition::CREATE_BLOG => [
'from' => BlogState::NEW_BLOG,
'to' => BlogState::NEED_REVIEW,
],
BlogTransition::PUBLISH => [
'from' => BlogState::NEED_REVIEW,
'to' => BlogState::PUBLISHED,
],
BlogTransition::NEED_REVIEW => [
'from' => BlogState::PUBLISHED,
'to' => BlogState::NEED_REVIEW,
],
BlogTransition::REJECT => [
'from' => BlogState::NEED_REVIEW,
'to' => BlogState::NEED_UPDATE,
],
BlogTransition::UPDATE => [
'from' => BlogState::NEED_UPDATE,
'to' => BlogState::NEED_REVIEW,
],
],
],
];

You can use PHP constants in YAML files via the !php/const notation. E.g. you can use !php/const App\Workflow\State\BlogState::NEW_BLOGinstead of 'new_blog' or !php/const App\Workflow\Transition\BlogTransition::CREATE_BLOGinstead of 'create_blog'.

Let break down some keys features:

  • blog: This is the name of our workflow which should be unique.
  • type: This is types of workflows that is supported by the components.
  • audit_trail: Setting the enabled option to true makes the application generate detailed log messages for the workflow activity.
  • marking_store: This is where you can set what will be the property and type in the entity where the state will be store.
  • supports: The entity that will be use for the workflow.
  • initial_marking: Where the workflow state is started.
  • places: All of the state that will be available in our workflow.
  • transitions: The section where you define all transition between from a state to another.
This is the structure of src folder, I use

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 BlogState
*
* @package App\Workflow\State
* @author Vandeth THO <thovandeth@gmail.com>
*/
final class BlogState
{
public const NEW_BLOG = 'new_blog';
public const NEED_REVIEW = 'need_review';
public const NEED_UPDATE = 'need_update';
public const PUBLISHED = 'published';
}

and for transition:

<?php
declare(strict_types=1);

namespace App\Workflow\Transition;
/**
* Class BlogTransition
*
* @package App\Workflow\Transition
* @author Vandeth THO <thovandeth@gmail.com>
*/
final class BlogTransition
{
public const CREATE_BLOG = 'create_blog';
public const UPDATE = 'update';
public const PUBLISH = 'publish';
public const REJECT = 'reject';
public const NEED_REVIEW = 'need_review';
}

for my model blog:

<?php
declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Blog extends Model
{
use HasFactory;

protected $table = 'blogs';

public $timestamps = true;

protected $fillable = [
'title',
'content',
'state'
];

protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'title' => 'string',
'content' => 'string',
'state' => 'string'
];

public function setState(string $value): void
{
$this->attributes['state'] = $value;
}

public function getState(): string|null
{
return $this->attributes['state'] ?? null;
}
}

The accessor and mutation function is really important here, otherwise the workflow component cannot access or mutate the value of state.

Register Workflow Service

In Laravel, you typically use a service provider to register and configure services. For the Symfony Workflow component, you’ll create a service provider to register the workflow configurations.

In your AppServiceProvider or a custom provider, you can register the workflow:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\MarkingStore\MethodMarkingStore;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\SupportStrategy\InstanceOfSupportStrategy;
use Symfony\Component\Workflow\Transition;
use Symfony\Component\Workflow\Workflow;

class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
$this->app->singleton(Registry::class, function () {
$registry = new Registry();

foreach (config('workflow') as $name => $workflowConfig) {
$transitions = [];
foreach ($workflowConfig['transitions'] as $transitionName => $transition) {
$transitions[] = new Transition($transitionName, $transition['from'], $transition['to']);
}
$definition = new Definition(
$workflowConfig['places'],
$transitions,
$workflowConfig['initial_marking']
);
$type = $workflowConfig['type'];
$markingStore = $workflowConfig['marking_store'];
$workflow = new Workflow(
$definition,
new MethodMarkingStore($type === 'state_machine', $markingStore['property']),
null,
$name
);
foreach ($workflowConfig['supports'] as $support) {
$registry->addWorkflow($workflow, new InstanceOfSupportStrategy($support));
}
}

return $registry;
});
}

/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}

Utilizing the Workflow in Your Application

With the workflow component registered, you can inject and use it in your services app/Workflow/BlogWorkflow.php:

<?php
declare(strict_types=1);

namespace App\Workflow;


use App\Models\Blog;
use App\Workflow\Transition\BlogTransition;
use Illuminate\Support\Facades\DB;
use LogicException;
use Symfony\Component\Workflow\Registry;

/**
* Class BlogWorkflow
*
* @package App\Workflow
* @author Vandeth THO <thovandeth@gmail.com>
*/
readonly class BlogWorkflow
{
/**
* BlogWorkflow constructor.
*
* @param Registry $workflowRegistry
*/
public function __construct(private Registry $workflowRegistry)
{}

/**
* Update the blog and send it to be reviewed
*
* @param string $title
* @param string $content
* @return Blog
*/
public function create(string $title, string $content): Blog
{
$blog = new Blog();
$blog->title = $title;
$blog->content = $content;
$blogStateMachine = $this->workflowRegistry->get($blog, 'blog_publishing');

$blogStateMachine->apply($blog, BlogTransition::CREATE_BLOG);
$blog->save();
return $blog;
}

/**
* Reject the blog and send it to be updated
*
* @param int $blogId
* @return Blog
*/
public function needReview(int $blogId): Blog
{
$blog = $this->getBlog($blogId);
$blogStateMachine = $this->workflowRegistry->get($blog, 'blog_publishing');

$blogStateMachine->apply($blog, BlogTransition::NEED_REVIEW);
$blog->save();
return $blog;
}

/**
* Reject the blog and send it to be updated
*
* @param int $blogId
* @return Blog
*/
public function reject(int $blogId): Blog
{
$blog = $this->getBlog($blogId);
$blogStateMachine = $this->workflowRegistry->get($blog, 'blog_publishing');

$blogStateMachine->apply($blog, BlogTransition::REJECT);
$blog->save();
return $blog;

}

/**
* Update the blog and send it to be reviewed
*
* @param int $blogId
* @param string|null $title
* @param string|null $content
* @return Blog
*/
public function update(int $blogId, ?string $title, ?string $content): Blog
{
$blog = $this->getBlog($blogId);
if ($title) {
$blog->title = $title;
}
if ($content) {
$blog->content = $content;
}
$blogStateMachine = $this->workflowRegistry->get($blog, 'blog_publishing');

$blogStateMachine->apply($blog, BlogTransition::UPDATE);
$blog->save();
return $blog;
}

/**
* Approve the blog and publish it
*
* @param int $blogId
* @return Blog
*/
public function publish(int $blogId): Blog
{
$blog = $this->getBlog($blogId);
$blogStateMachine = $this->workflowRegistry->get($blog, 'blog_publishing');

$blogStateMachine->apply($blog, BlogTransition::PUBLISH);
$blog->save();
return $blog;
}

private function getBlog(int $blogId): Blog
{
$blog = DB::table('blogs')->find($blogId);

if ($blog) {
return $blog;
}

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

Unit Testing Workflows

  1. Preparing for Testing

Before writing tests, make sure your testing environment is set up correctly. Laravel typically uses PHPUnit for testing, and you should have a phpunit.xml file in your project root.

2. Writing a Test Case

Create a new test case using Laravel’s Artisan command:

php artisan make:test BlogWorkflowTest --unit

This command creates a new test file in the tests/Unit directory.

3. Writing Test Methods

Here’s an example of how your BlogPostWorkflowTest might look:

<?php
declare(strict_types=1);

namespace App\Workflow;


use App\Models\Blog;
use App\Workflow\State\BlogState;
use App\Workflow\Transition\BlogTransition;
use Illuminate\Support\Facades\DB;
use LogicException;
use Symfony\Component\Workflow\Registry;

/**
* Class BlogWorkflow
*
* @package App\Workflow
* @author Vandeth THO <thovandeth@gmail.com>
*/
readonly class BlogWorkflow
{
/**
* BlogWorkflow constructor.
*
* @param Registry $workflowRegistry
*/
public function __construct(private Registry $workflowRegistry)
{}

/**
* Update the blog and send it to be reviewed
*
* @param string $title
* @param string $content
* @return Blog
*/
public function create(string $title, string $content): Blog
{
$blog = new Blog();
$blog->title = $title;
$blog->content = $content;

$blogStateMachine = $this->workflowRegistry->get($blog, 'blog_publishing');
$blogStateMachine->apply($blog, BlogTransition::CREATE_BLOG);
$blog->save();
return $blog;
}

/**
* Reject the blog and send it to be updated
*
* @param int $blogId
* @return Blog
*/
public function needReview(int $blogId): Blog
{
$blog = $this->getBlog($blogId);
$blogStateMachine = $this->workflowRegistry->get($blog, 'blog_publishing');

$blogStateMachine->apply($blog, BlogTransition::NEED_REVIEW);
$blog->save();
return $blog;
}

/**
* Reject the blog and send it to be updated
*
* @param int $blogId
* @return Blog
*/
public function reject(int $blogId): Blog
{
$blog = $this->getBlog($blogId);
$blogStateMachine = $this->workflowRegistry->get($blog, 'blog_publishing');

$blogStateMachine->apply($blog, BlogTransition::REJECT);
$blog->save();
return $blog;

}

/**
* Update the blog and send it to be reviewed
*
* @param int $blogId
* @param string|null $title
* @param string|null $content
* @return Blog
*/
public function update(int $blogId, ?string $title, ?string $content): Blog
{
$blog = $this->getBlog($blogId);
if ($title) {
$blog->title = $title;
}
if ($content) {
$blog->content = $content;
}
$blogStateMachine = $this->workflowRegistry->get($blog, 'blog_publishing');

$blogStateMachine->apply($blog, BlogTransition::UPDATE);
$blog->save();
return $blog;
}

/**
* Approve the blog and publish it
*
* @param int $blogId
* @return Blog
*/
public function publish(int $blogId): Blog
{
$blog = $this->getBlog($blogId);
$blogStateMachine = $this->workflowRegistry->get($blog, 'blog_publishing');

$blogStateMachine->apply($blog, BlogTransition::PUBLISH);
$blog->save();
return $blog;
}

private function getBlog(int $blogId): Blog
{
$blog = Blog::find($blogId);

if ($blog) {
return $blog;
}

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

By following these steps, you’ll be able to implement and manage workflows in your Laravel application effectively.

--

--

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.