juanparati/laravel-sync-workflow

A library for executing synchronous workflows in Laravel

Maintainers

Package info

github.com/juanparati/laravel-sync-workflow

pkg:composer/juanparati/laravel-sync-workflow

Statistics

Installs: 545

Dependents: 0

Suggesters: 0

Stars: 2

Open Issues: 0

1.1 2026-03-24 09:13 UTC

This package is auto-updated.

Last update: 2026-03-24 09:14:22 UTC


README

test

Laravel Synchronous Workflows

A robust library for executing reproducible synchronous workflows with seamless event sourcing capabilities in Laravel.

Workflow activities are executed sequentially within a single process and are not distributed across multiple instances or jobs.

Each time that an activity is executed, its input is passed to the next activity in the workflow, and objects are decoupled from their original reference to avoid non-desired mutability.

For distributed asynchronous workflows, see Laravel Workflow.

Key features include:

  • Synchronous workflow execution
  • Event sourcing capabilities
  • Comprehensive workflow history tracking
  • Workflow replay functionality
  • Automatic object reference decoupling
  • Relative time management
  • Unique workflow locking
  • Controlled exceptions for graceful workflow halting

This library is inspired by Laravel Workflow and Laravel Saga.

Installation

composer require juanparati/laravel-sync-workflow

Publish migrations and configuration files (required for event sourcing):

artisan vendor:publish --tag=laravel-sync-workflow

Run migrations:

artisan migrate

Usage

Basic Workflow Example

Here's a simple workflow that processes user registration:

<?php

namespace App\SyncWorkflows;

use App\SyncWorkflows\UserRegistration\SendWelcomeEmail;
use App\SyncWorkflows\UserRegistration\CreateUserProfile;
use Juanparati\SyncWorkflow\SyncWorkflow;

class UserRegistrationWorkflow extends SyncWorkflow
{
    protected User $user;

    public function __construct(User|array $user)
    {
        $this->user = $user instanceof User ? $user : new User($user);
    }

    public function handle()
    {
        // Create user profile
        $profile = $this->executor()->runActivity(
            CreateUserProfile::class,
            $this->user
        );

        // Send welcome email
        $this->executor()->runActivity(
            SendWelcomeEmail::class,
            $this->user
        );

        return ['user_id' => $profile->id, 'status' => 'registered'];
    }
}

or as a chain of activities:

<?php

namespace App\SyncWorkflows;

use App\SyncWorkflows\UserRegistration\SendWelcomeEmail;
use App\SyncWorkflows\UserRegistration\CreateUserProfile;
use Juanparati\SyncWorkflow\Contracts\WithEventSourcing;
use Juanparati\SyncWorkflow\SyncWorkflow;

// When implementing WithEventSourcing, the workflow will be persisted in the database
// and its execution history will be available for replay.
class UserRegistrationWorkflow extends SyncWorkflow implements WithEventSourcing
{

    protected User $user;

    public function __construct(User|array $user)
    {
        $this->user = $user instanceof User ? $user : new User($user);
    } 
       
    public function handle()
    {
        $profile = $this->executor()->runChainedActivities([
            CreateUserProfile::class,
            SendWelcomeEmail::class,
        ], $this->user);    // The output of one activity is the input of the next
              
        return ['user_id' => $profile->id, 'status' => 'registered'];
    }
}

Activity Example

Activities contain the actual business logic:

<?php

namespace App\SyncWorkflows\UserRegistration;

use App\Models\User;
use Juanparati\SyncWorkflow\SyncActivity;

class CreateUserProfile extends SyncActivity
{
    public function __construct(protected User $user) {}

    public function handle()
    {   
        // Use relativeNow instead of now() to ensure consistent timestamps during workflow replay
        // by preserving the original execution time.
        $this->user->created_at = $this->executor()->relativeNow();
        $this->user->email_verified = true;
        $this->user->save();
            
        return $this->user;
    }
}

Running Workflows

Execute a workflow programmatically:

use Juanparati\SyncWorkflow\SyncExecutor;

$result = SyncExecutor::dispatch(
    new UserRegistrationWorkflow(['email' => 'user@example.com', 'name' => 'John Doe'])
);

// Access the result
echo "User registered with ID: " . $result->id;

or alternatively:

use Juanparati\SyncWorkflow\SyncExecutor;

$workflow = SyncExecutor::make()
    ->load(new UserRegistrationWorkflow(['email' => 'user@example.com', 'name' => 'John Doe']));
    
echo "Workflow ID: " . $workflow->getId();

$workflow->run();

echo "Workflow finished at " . $workflow->getExecutionTime()['endedAt'];

$result = $workflow->getResult();

// Access the result
echo "User registered with ID: " . $result->id;

Controlled Exceptions

To gracefully halt workflow execution, you can throw a SyncWorkflowControlledException from within an activity:

<?php

namespace App\SyncWorkflows\OrderProcessing;

use Juanparati\SyncWorkflow\Exceptions\SyncWorkflowControlledException;
use Juanparati\SyncWorkflow\SyncActivity;
use App\Services\PaymentService;
use Exception;

class ValidatePayment extends SyncActivity
{
    public function handle()
    {   
        $paymentPermission = PaymentService::obtainPermission($this->input);
        
        if (!$paymentPermission) {
            throw (new SyncWorkflowControlledException('Permission denied'))
                ->addError(['info' => $this->input]);
        }
        
        return $paymentPermission;                
    }
}

You can handle the exception in your workflow:

try {
    SyncExecutor::dispatch(new OrderProcessingWorkflow($order));
} catch (SyncWorkflowControlledException $e) {
    \Log::warning('Order process cancelled: ' . $e->getMessage(), $e->getErrors());
} catch (Exception $e) {
    \Log::error('Unable to process order: ' . $e->getMessage());   
    throw $e;
}

Workflow locking

Use the HasLock trait to automatically acquire a lock before executing a workflow:

<?php

namespace App\SyncWorkflows;

use App\SyncWorkflows\UserRegistration\SendWelcomeEmail;
use App\SyncWorkflows\UserRegistration\CreateUserProfile;
use Juanparati\SyncWorkflow\Concerns\HasLock;
use Juanparati\SyncWorkflow\SyncWorkflow;

class UserRegistrationWorkflow extends SyncWorkflow
{
    use HasLock;

    protected function uniqueId() {
        return 'my_lock_key';
    }
    ...
}

Use the uniqueId method to define a unique identifier for the workflow, otherwise the class name is used by default.

When the lock was already acquired by another workflow the exception SyncWorkflowLockException is thrown.

Commands

Generate a new workflow

artisan make:sync-workflow MyWorkflow

The workflow will be created in the app/SyncWorkflows directory.

Generate a new activity

artisan make:sync-workflow-activity MyWorkflow/MyFirstActivity

Replay a workflow

artisan sync-workflow:replay [workflow-id]

View workflow state

artisan sync-workflow:view [workflow-id]