juanparati / laravel-sync-workflow
A library for executing synchronous workflows in Laravel
Requires
- php: >=8.3
- ext-pdo: *
- illuminate/support: >=11.1
- ramsey/uuid: ^4.7
Requires (Dev)
- laravel/pint: ^1.22
- orchestra/testbench: ^10.4
- phpunit/phpunit: ^11.5
This package is auto-updated.
Last update: 2025-08-29 13:00:25 UTC
README
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
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\SyncWorkflows\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\SyncWorkflows\Contracts\WithEventSourcing; use Juanparati\SyncWorkflows\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\SyncWorkflows\SyncActivity; class CreateUserProfile extends SyncActivity { public function __construct(protected User $user); public function execute() { // 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\SyncWorkflows\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\SyncWorkflows\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\SyncWorkflows\Exceptions\SyncWorkflowControlledException; use Juanparati\SyncWorkflows\SyncActivity; use App\Services\PaymentService; use Exception; class ValidatePayment extends SyncActivity { public function execute() { $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; }
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]