chevere / workflow
Workflow procedures for PHP
Installs: 1 126
Dependents: 1
Suggesters: 0
Security: 0
Stars: 23
Watchers: 1
Forks: 1
Open Issues: 1
Requires
- php: ^8.1
- amphp/parallel: ^1.4
- chevere/action: ^1.0.0
- chevere/data-structure: ^1.0.1
- chevere/parameter: ^1.0.1
- chevere/regex: ^1.0.1
- ramsey/uuid: ^4.7
Requires (Dev)
- chevere/filesystem: ^1.0.x-dev
- chevere/var-dump: ^1.0
- phpstan/phpstan: ^1.9
- phpunit/phpunit: ^9.5
- symplify/easy-coding-standard: ^11.1
README
Summary
A Workflow is a configurable stored procedure that will run one or more jobs. Jobs are independent from each other, but interconnected as you can pass response references between jobs. Jobs supports conditional running based on variables and previous job responses.
Installing
Workflow is available through Packagist and the repository source is at chevere/workflow.
composer require chevere/workflow
What it does?
The Workflow package provides tooling for defining an execution procedure based on the workflow pattern. Its purpose is to abstract logic instructions as units of interconnected independent jobs.
Instead of building a monolithic procedure, you define a Workflow made of jobs, enabling developers to easy test and maintain re-usable multi-purpose logic.
::: tip 💡 Workflow introduction Read Workflow for PHP at Rodolfo's blog for a compressive introduction to this package. :::
How to use
Workflow provides the following functions at the Chevere\Workflow
namespace. Use these functions to define a Workflow, its variables and response references for named jobs.
- A Job is defined by its Action
- Jobs are independent from each other, define shared variables using function
variable()
- Reference {job#A response} -> {job#B input} by using function
response()
To produce logic with this package:
- Create a Workflow using function
workflow
- Define jobs using function
sync
orasync
- Run the Workflow using function
run
Creating Workflow
To create a Workflow define its named Jobs.
A Job is created by passing an Action and its expected run arguments which can be raw values, Variables and/or Responses to another job's output.
The syntax for writing Workflow jobs require name
for job's name, sync/async
depending on job run method, and named parameter
binding for each Action::main
parameter.
<name>: <sync|async>(
<action>,
<parameter>: <variable|reference|raw>,
)
For example, for the given MyAction
action:
use function Chevere\Action\Action; class MyAction extends Action { protected function main(string $foo, string $bar): array { return []; } }
You will be able to write a Workflow like this:
use function Chevere\Workflow\sync; workflow( greet: sync( new MyAction(), foo: variable('super'), bar: variable('taldo'), ) );
With synchronous jobs
Use function sync
to create a synchronous job, which block execution until it gets resolved.
In the example below a Workflow describes an image uploading procedure.
use function Chevere\Workflow\sync; use function Chevere\Workflow\response; use function Chevere\Workflow\variable; use function Chevere\Workflow\workflow; workflow( user: sync( new GetUser(), request: variable('payload') ), validate: sync( new ValidateImage(), mime: 'image/png', file: variable('file') ), meta: sync( new GetMeta(), file: variable('file'), ), store: sync( new StoreFile(), file: variable('file'), name: response('meta', 'name'), user: response('user') ), );
variable('payload')
andvariable('file')
declares a Variable.response('meta', 'name')
andresponse('user')
declares a Response reference.
The graph for this Workflow says that all jobs run one after each other as all jobs are defined using sync
.
$workflow->jobs()->graph()->toArray(); // contains [ ['user'], ['validate'], ['meta'], ['store'] ];
To complete the example, here's how to Run the Workflow previously defined:
use function Chevere\Workflow\run; run( $workflow, payload: $_REQUEST, file: '/path/to/file', );
With asynchronous jobs
Use function async
to create an asynchronous job, which runs in parallel non-blocking.
In the example below a Workflow describes an image creation procedure for multiple image sizes.
use function Chevere\Workflow\sync; use function Chevere\Workflow\response; use function Chevere\Workflow\variable; use function Chevere\Workflow\workflow; workflow( thumb: async( new ImageResize(), image: variable('image'), width: 100, height: 100, fit: 'thumb' ), medium: async( new ImageResize(), image: variable('image'), width: 500, fit: 'resizeByW' ), store: sync( new StoreFiles(), response('thumb', 'filename'), response('medium', 'filename'), ), );
variable('image')
declares a Variable.response('thumb', 'filename')
andresponse('medium', 'filename')
declares a Response reference.
The graph for this Workflow says that thumb
, medium
and poster
run non-blocking in parallel. Job store
runs blocking (another node).
$workflow->jobs()->graph()->toArray(); // contains [ ['thumb', 'medium', 'poster'], ['store'] ];
To complete the example, here's how to Run the Workflow previously defined:
use function Chevere\Workflow\run; run( workflow: $workflow, arguments: [ 'image' => '/path/to/file', ] );
Variable
Use function variable
to declare a Workflow variable. This denotes a variable which must be injected by at Workflow run layer.
use function Chevere\Workflow\variable; variable('myVar');
Response
Use function response
to declare a Job response reference to a response returned by a previous Job.
🪄 When using a response it will auto declare the referenced Job as dependency.
use function Chevere\Workflow\response; response(job: 'task');
References can be also made on a response member identified by key
.
use function Chevere\Workflow\response; response(job: 'task', key: 'name');
Creating Job
The Job
class defines an Action with arguments which can be passed passed "as-is", variable or response on constructor using named arguments.
Synchronous job
use function Chevere\Workflow\job; sync( new SomeAction(), ...$argument );
Asynchronous job
use function Chevere\Workflow\job; async( new SomeAction(), ...$argument );
Note: Actions must support serialization for being used on async
jobs. For not serializable Actions as these interacting with connections (namely streams, database clients, etc.) you should use sync
job.
Job variables and references
sync( new SomeAction(), context: 'public', role: variable('role'), userId: response('user', 'id'), );
For the code above, argument context
will be passed "as-is" (public
) to SomeAction
, arguments role
and userId
will be dynamic provided. When running the Workflow these arguments will be matched against the Parameters defined at the main method for SomeAction
.
Conditional running
Method withRunIf
enables to pass arguments of type Variable or Response for conditionally running a Job.
sync( new CompressImage(), file: variable('file') ) ->withRunIf( variable('compressImage'), response('SomeAction', 'doImageCompress') )
For the code above, all conditions must meet to run the Job and both variable compressImage
and the reference SomeAction:doImageCompress
must be true
to run the job.
Dependencies
Use withDepends
method to explicit declare previous jobs as dependencies. The dependent Job won't run until the dependencies are resolved.
job(new SomeAction()) ->withDepends('myJob');
Running a Workflow
To run a Workflow use the run
function by passing a Workflow and an array
for its variables (if any).
use function Chevere\Workflow\run; $run = run($workflow, ...$variables);
Use getResponse
to retrieve a job response as a CastArgument
object which can be used to get a typed response.
$string = $run->getResponse('myJob')->string();
Code Examples
Hello, world
Run live example: php demo/hello-world.php Rodolfo
- view source
The basic example Workflow defines a greet for a given username. The job greet
is a named argument and it takes the GreetAction
plus its main method arguments. The run
function is used to execute the Workflow.
use Chevere\Demo\Actions\Greet; use function Chevere\Workflow\run; use function Chevere\Workflow\sync; use function Chevere\Workflow\variable; use function Chevere\Workflow\workflow; $workflow = workflow( greet: sync( new Greet(), username: variable('username'), ), ); $run = run( $workflow, username: $argv[1] ?? 'World' ); $greet = $run->getReturn('greet')->string(); echo <<<PLAIN {$greet} PLAIN;
The graph for this Workflow contains only the greet
job.
$workflow->jobs()->graph()->toArray(); // contains [ ['greet'], ];
Image resize example (async)
Run live example: php demo/image-resize.php
- view source
For this example Workflow defines an image resize procedure in two sizes. All jobs are defined as async, but as there are dependencies between jobs (see variable
and response
) the system resolves a suitable run strategy.
use Chevere\Demo\Actions\ImageResize; use Chevere\Demo\Actions\StoreFile; use function Chevere\Workflow\async; use function Chevere\Workflow\response; use function Chevere\Workflow\run; use function Chevere\Workflow\variable; use function Chevere\Workflow\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/', );
The graph for the Workflow above shows that thumb
and poster
run async, just like storeThumb
and storePoster
but the store*
jobs run after the first dependency level gets resolved.
$workflow->jobs()->graph()->toArray(); // contains [ ['thumb', 'poster'], ['storeThumb', 'storePoster'] ];
Use function run
to run the Workflow, variables are passed as named arguments.
use function Chevere\Workflow\run; $run = run( $workflow, image: '/path/to/image-to-upload.png', savePath: '/path/to/storage/' );
Use getReturn
to retrieve a job response as a CastArgument
object which can be used to get a typed response.
$thumbFile = $run->getReturn('thumb')->string();
Sync vs Async
Run live example: php demo/sync-vs-async.php
- view source
For this example you can compare the execution time between synchronous and asynchronous jobs. The example fetches the content of three URLs using FetchUrl
action.
use Chevere\Demo\Actions\FetchUrl; use function Chevere\Workflow\async; use function Chevere\Workflow\run; use function Chevere\Workflow\sync; use function Chevere\Workflow\variable; use function Chevere\Workflow\workflow; $sync = workflow( php: sync( new FetchUrl(), url: variable('php'), ), github: sync( new FetchUrl(), url: variable('github'), ), chevere: sync( new FetchUrl(), url: variable('chevere'), ), ); $async = workflow( php: async( new FetchUrl(), url: variable('php'), ), github: async( new FetchUrl(), url: variable('github'), ), chevere: async( new FetchUrl(), url: variable('chevere'), ), ); $variables = [ 'php' => 'https://www.php.net', 'github' => 'https://github.com/chevere/workflow', 'chevere' => 'https://chevere.org', ]; $time = microtime(true); $run = run($sync, ...$variables); $time = microtime(true) - $time; echo "Time sync: {$time}\n"; $time = microtime(true); $run = run($async, ...$variables); $time = microtime(true) - $time; echo "Time async: {$time}\n";
When running sync (blocking) jobs the execution time is higher than async (non-blocking) jobs. This is because async jobs run in parallel.
Time sync: 2.5507028102875
Time async: 1.5810508728027
Conditional jobs
Run live example: php demo/run-if.php
- view source
For this example Workflow defines a greet for a given username, but only if a sayHello
variable is set to true
.
use Chevere\Demo\Actions\Greet; use function Chevere\Workflow\run; use function Chevere\Workflow\sync; use function Chevere\Workflow\variable; use function Chevere\Workflow\workflow; /* php demo/run-if.php Rodolfo php demo/run-if.php */ $workflow = workflow( greet: sync( new Greet(), username: variable('username'), )->withRunIf( variable('sayHello') ), );
Method withRunIf
accepts one or more variable
and response
references. All conditions must be true at the same time for the job to run.
Debugging
When working with this package you may want to debug the Workflow to ensure that the jobs are declared as expected.
To debug a Workflow inspect the Jobs graph. It will show the job names and their dependencies for each execution level.
$workflow->jobs()->graph()->toArray(); [ ['job1', 'job2'], // 1st level ['job3', 'job4'], // 2nd level ['job5'], // 3rd level ];
For each level jobs will run in parallel, but the next level will run after the previous level gets resolved.
Note
For runtime debugging is strongly recommended to use a non-blocking debugger like xrDebug.
Testing
Workflow checks on variables, references and any other configuration so you don't have to worry about that.
Testing the Workflow itself is not necessary as it's just a configuration. What you need to test is the Workflow definition and their Jobs (Actions).
Testing Workflow
For testing a Workflow what you need to assert is the expected Workflow graph (execution order).
assertSame( $expectedGraph, $workflow->jobs()->graph()->toArray() );
Testing Job
For testing a Job what you need to test is the Action that defines that given Job against the response from the Action main
method.
$action = new MyAction(); assertSame( $expected, $action->main(...$arguments) );
Documentation
Documentation is available at chevere.org/packages/workflow.
License
Copyright Rodolfo Berrios A.
This software is licensed under the Apache License, Version 2.0. See LICENSE for the full license text.
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.