This page showcases real-world examples from the Chevere Workflow repository, demonstrating common patterns and best practices.
Hello world
The simplest workflow example using an Action class:
use Chevere\Demo\Actions\ Greet ;
use function Chevere\Workflow\ { run , sync , variable , workflow };
$workflow = workflow (
greet : sync (
new Greet (),
username : variable ( 'username' ),
),
);
$run = run (
$workflow ,
username : $argv [ 1 ] ?? 'World'
);
echo $run -> response ( 'greet' ) -> string ();
// Output: Hello, World!
The Greet action:
use Chevere\Action\ Action ;
use Chevere\Parameter\Interfaces\ StringParameterInterface ;
use function Chevere\Parameter\ string ;
class Greet extends Action
{
public function __invoke ( string $username ) : string
{
return "Hello, { $username }!" ;
}
public static function acceptReturn () : StringParameterInterface
{
return string ( '/ ^ Hello, /' );
}
}
The acceptReturn() method defines validation rules for the return value using chevere/parameter. The response must be a string starting with “Hello, ”.
Chained jobs
Passing data between jobs using response():
use Chevere\Demo\Actions\ MyAction ;
use function Chevere\Workflow\ { response , run , sync , variable , workflow };
$workflow = workflow (
greet : sync (
MyAction :: class ,
foo : variable ( 'super' ),
),
capo : sync (
MyAction :: class ,
foo : response ( 'greet' ),
),
wea : sync (
function ( string $foo ) {
return "Wea, { $foo }" ;
},
foo : response ( 'greet' ),
),
);
$hello = run (
$workflow ,
super : 'Chevere' ,
);
echo $hello -> response ( 'greet' ) -> string () . PHP_EOL ;
// Hello, Chevere
echo $hello -> response ( 'capo' ) -> string () . PHP_EOL ;
// Hello, Hello, Chevere
echo $hello -> response ( 'wea' ) -> string () . PHP_EOL ;
// Wea, Hello, Chevere
use Chevere\Action\ Action ;
class MyAction extends Action
{
public function __invoke ( string $foo ) : string
{
return "Hello, { $foo }" ;
}
}
Using closures
Simple calculations using inline closures:
use function Chevere\Workflow\ { response , run , sync , variable , workflow };
$workflow = workflow (
calculate : sync (
function ( int $a , int $b ) : int {
return $a + $b ;
},
a : 10 ,
b : variable ( 'value' )
),
format : sync (
fn ( int $result ) : string => "Result: { $result }" ,
result : response ( 'calculate' )
)
);
$run = run ( $workflow , value : 5 );
echo $run -> response ( 'format' ) -> string () . PHP_EOL ;
// Result: 15
Closures are perfect for simple, one-off operations. For complex or reusable logic, use Action classes instead.
Parallel image processing
Resize images in parallel, then store the results:
use Chevere\Demo\Actions\ { ImageResize , StoreFile };
use function Chevere\Workflow\ { async , response , run , variable , workflow };
$workflow = workflow (
thumb : async (
new ImageResize (),
file : variable ( 'image' ),
fit : 'thumbnail' ,
),
poster : async (
new ImageResize (),
file : variable ( 'image' ),
fit : 'poster' ,
),
storeThumb : async (
new StoreFile (),
file : response ( 'thumb' ),
dir : variable ( 'saveDir' ),
),
storePoster : async (
new StoreFile (),
file : response ( 'poster' ),
dir : variable ( 'saveDir' ),
)
);
$run = run (
$workflow ,
image : __DIR__ . '/src/php.jpeg' ,
saveDir : __DIR__ . '/src/output/' ,
);
// View execution graph
$graph = $run -> workflow () -> jobs () -> graph () -> toArray ();
echo "Workflow graph: \n " ;
foreach ( $graph as $level => $jobs ) {
echo " { $level }: " . implode ( '|' , $jobs ) . " \n " ;
}
// Output:
// Workflow graph:
// 0: thumb|poster
// 1: storeThumb|storePoster
ImageResize action
demo/Actions/ImageResize.php
use Chevere\Action\ Action ;
use Chevere\Parameter\Attributes\ _string ;
use RuntimeException ;
class ImageResize extends Action
{
public const FIT_WIDTH = [
'thumbnail' => 50 ,
'poster' => 150 ,
];
public function __invoke (
#[ _string ( '/ \. jpe?g $ /' )]
string $file ,
string $fit
) : string {
[ $width , $height ] = getimagesize ( $file );
$targetWidth = self :: FIT_WIDTH [ $fit ];
$targetHeight = intval ( $height / $width * $targetWidth );
$image = imagecreatetruecolor ( $targetWidth , $targetHeight );
$source = imagecreatefromjpeg ( $file );
imagecopyresampled (
$image , $source , 0 , 0 , 0 , 0 ,
$targetWidth , $targetHeight , $width , $height
);
$pos = strrpos ( $file , '.' );
$target = substr ( $file , 0 , $pos )
. ".{ $fit }"
. substr ( $file , $pos );
return imagejpeg ( $image , $target )
? $target
: throw new RuntimeException ( 'Unable to save image' );
}
}
The #[_string('/\.jpe?g$/')] attribute validates that the file parameter ends with .jpg or .jpeg.
Conditional execution
Run jobs based on runtime conditions:
use Chevere\Demo\Actions\ Greet ;
use function Chevere\Workflow\ { run , sync , variable , workflow };
$workflow = workflow (
greet : sync (
new Greet (),
username : variable ( 'username' ),
) -> withRunIf (
variable ( 'sayHello' )
),
);
$name = $argv [ 1 ] ?? '' ;
$run = run (
$workflow ,
username : $name ,
sayHello : $name !== ''
);
if ( $run -> skip () -> contains ( 'greet' )) {
exit ;
}
echo $run -> response ( 'greet' ) -> string ();
Usage:
php demo/run-if.php Rodolfo
# Output: Hello, Rodolfo!
php demo/run-if.php
# No output (job skipped)
Compare synchronous and asynchronous execution:
use Chevere\Demo\Actions\ FetchUrl ;
use function Chevere\Workflow\ { async , run , sync , variable , workflow };
$sync = workflow (
job1 : sync (
new FetchUrl (),
url : variable ( 'php' ),
),
job2 : sync (
new FetchUrl (),
url : variable ( 'github' ),
),
job3 : sync (
new FetchUrl (),
url : variable ( 'chevere' ),
),
);
$async = workflow (
job1 : async (
new FetchUrl (),
url : variable ( 'php' ),
),
job2 : async (
new FetchUrl (),
url : variable ( 'github' ),
),
job3 : async (
new FetchUrl (),
url : variable ( 'chevere' ),
),
);
$variables = [
'php' => 'https://www.php.net' ,
'github' => 'https://github.com/chevere/workflow' ,
'chevere' => 'https://chevere.org' ,
];
// Measure sync execution
$time = microtime ( true );
$run = run ( $sync , ... $variables );
$time = round (( microtime ( true ) - $time ) * 1000 );
echo "Time (ms) sync: { $time } \n " ;
// Measure async execution
$time = microtime ( true );
$run = run ( $async , ... $variables );
$time = round (( microtime ( true ) - $time ) * 1000 );
echo "Time (ms) async: { $time } \n " ;
Typical output:
Time (ms) sync: 842
Time (ms) async: 312
Async jobs execute in parallel when they have no dependencies, significantly reducing total execution time for I/O-bound operations.
Complex dependency graph
Building a multi-level workflow with mixed dependencies:
use function Chevere\Workflow\ { workflow , async , sync , response , variable };
$workflow = workflow (
// Level 0: Independent jobs run in parallel
fetchUser : async (
FetchUser :: class ,
userId : variable ( 'userId' )
),
fetchSettings : async (
FetchSettings :: class ,
userId : variable ( 'userId' )
),
fetchPosts : async (
FetchPosts :: class ,
userId : variable ( 'userId' )
),
// Level 1: Jobs that depend on level 0
enrichUser : async (
EnrichUserData :: class ,
user : response ( 'fetchUser' ),
settings : response ( 'fetchSettings' )
),
// Level 2: Sequential job waits for enrichUser
processUser : sync (
ProcessUserData :: class ,
user : response ( 'enrichUser' )
),
// Level 3: Final jobs run in parallel after processUser
sendEmail : async (
SendWelcomeEmail :: class ,
email : response ( 'processUser' , 'email' )
),
updateCache : async (
UpdateUserCache :: class ,
userId : variable ( 'userId' ),
data : response ( 'processUser' )
)
);
$graph = $workflow -> jobs () -> graph () -> toArray ();
print_r ( $graph );
// [
// ['fetchUser', 'fetchSettings', 'fetchPosts'], // Level 0
// ['enrichUser'], // Level 1
// ['processUser'], // Level 2
// ['sendEmail', 'updateCache'] // Level 3
// ]
User registration flow
A complete user registration workflow:
use function Chevere\Workflow\ { workflow , sync , async , variable , response , run };
$workflow = workflow (
validate : sync (
ValidateRegistration :: class ,
email : variable ( 'email' ),
password : variable ( 'password' )
),
createUser : sync (
CreateUser :: class ,
email : variable ( 'email' ),
passwordHash : response ( 'validate' , 'hash' )
),
sendWelcome : async (
SendWelcomeEmail :: class ,
userId : response ( 'createUser' , 'id' ),
email : variable ( 'email' )
),
logEvent : async (
LogRegistration :: class ,
userId : response ( 'createUser' , 'id' ),
timestamp : response ( 'createUser' , 'createdAt' )
),
createProfile : async (
CreateUserProfile :: class ,
userId : response ( 'createUser' , 'id' )
)
);
$result = run (
$workflow ,
email : '[email protected] ' ,
password : 'secure_password_123'
);
return [
'userId' => $result -> response ( 'createUser' , 'id' ) -> int (),
'email' => $result -> response ( 'createUser' , 'email' ) -> string (),
'profileCreated' => ! $result -> skip () -> contains ( 'createProfile' ),
'emailSent' => ! $result -> skip () -> contains ( 'sendWelcome' )
];
Content processing pipeline
Conditional processing with translation:
use function Chevere\Workflow\ { workflow , sync , async , variable , response , run };
$workflow = workflow (
analyze : sync (
AnalyzeContent :: class ,
content : variable ( 'text' )
),
moderate : sync (
ModerateContent :: class ,
analysis : response ( 'analyze' )
),
translate : async (
TranslateContent :: class ,
text : variable ( 'text' ),
targetLang : variable ( 'lang' )
) -> withRunIf (
variable ( 'needsTranslation' )
),
generateSummary : async (
GenerateSummary :: class ,
content : variable ( 'text' )
),
publish : sync (
PublishContent :: class ,
original : variable ( 'text' ),
translated : response ( 'translate' ),
summary : response ( 'generateSummary' ),
moderation : response ( 'moderate' )
)
);
$result = run (
$workflow ,
text : 'Hello world, this is a test article...' ,
lang : 'es' ,
needsTranslation : true
);
if ( $result -> skip () -> contains ( 'translate' )) {
echo "Translation was skipped \n " ;
} else {
echo "Translated to: " . $result -> response ( 'translate' ) -> string () . " \n " ;
}
Testing workflow graphs
From the test suite - verifying execution order:
use PHPUnit\Framework\ TestCase ;
use Chevere\Workflow\ Graph ;
use function Chevere\Workflow\ async ;
class GraphTest extends TestCase
{
public function testMixedParallelAndSequential () : void
{
$graph = new Graph ();
$graph = $graph -> withPut (
'jn' ,
$this -> getJob () -> withDepends ( 'j0' , 'j1' )
);
$graph = $graph -> withPut (
'jx' ,
$this -> getJob () -> withDepends ( 'j0' , 'j1' )
);
$graph = $graph -> withPut ( 'j0' , $this -> getJob () -> withIsSync ( true ));
$graph = $graph -> withPut ( 'j1' , $this -> getJob () -> withIsSync ( true ));
$this -> assertSame (
[
[ 'j0' ], // Level 0: First sync job
[ 'j1' ], // Level 1: Second sync job
[ 'jn' , 'jx' ], // Level 2: Parallel async jobs
],
$graph -> toArray ()
);
}
}
Use graph()->toArray() to inspect and verify the execution order in your tests.
Best practices from examples
Use async for I/O operations
Network requests, file operations, and database queries benefit from async execution:
workflow (
fetchApi1 : async ( FetchApi :: class , url : 'https://api1.com' ),
fetchApi2 : async ( FetchApi :: class , url : 'https://api2.com' ),
combine : sync (
CombineResults :: class ,
a : response ( 'fetchApi1' ),
b : response ( 'fetchApi2' )
)
)
Use sync for sequential operations
When order matters or resources conflict, use sync:
workflow (
lock : sync ( AcquireLock :: class , resource : variable ( 'id' )),
update : sync ( UpdateResource :: class , id : variable ( 'id' )),
unlock : sync ( ReleaseLock :: class , resource : variable ( 'id' ))
)
Each action should have a single responsibility:
// Good: Focused actions
workflow (
validate : sync ( ValidateEmail :: class , email : variable ( 'email' )),
send : sync ( SendEmail :: class , to : variable ( 'email' ))
)
// Avoid: Do-everything actions
workflow (
process : sync ( ValidateAndSendEmail :: class , email : variable ( 'email' ))
)
Use conditional execution wisely
Skip expensive operations when not needed:
workflow (
checkCache : sync ( CheckCache :: class , key : variable ( 'key' )),
fetchData : async (
FetchFromDatabase :: class ,
key : variable ( 'key' )
) -> withRunIfNot (
response ( 'checkCache' , 'found' )
)
)