{"id":15026635,"url":"https://github.com/wol-soft/php-workflow","last_synced_at":"2025-04-09T20:21:13.049Z","repository":{"id":41972890,"uuid":"394952204","full_name":"wol-soft/php-workflow","owner":"wol-soft","description":"Create controlled workflows from small pieces","archived":false,"fork":false,"pushed_at":"2024-11-25T12:17:00.000Z","size":95,"stargazers_count":24,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-23T22:12:54.422Z","etag":null,"topics":["error-handling","modularization","php","php74","php8","reusable-components","workflow","workflow-engine"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wol-soft.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-08-11T10:35:49.000Z","updated_at":"2025-03-06T23:52:41.000Z","dependencies_parsed_at":"2025-02-15T20:32:51.514Z","dependency_job_id":"81a1df2c-04f9-4d0a-9431-efc2acf5402c","html_url":"https://github.com/wol-soft/php-workflow","commit_stats":{"total_commits":44,"total_committers":3,"mean_commits":"14.666666666666666","dds":"0.20454545454545459","last_synced_commit":"43ce48b8fd6f0924aae21326f032fb1d1203997f"},"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wol-soft%2Fphp-workflow","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wol-soft%2Fphp-workflow/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wol-soft%2Fphp-workflow/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wol-soft%2Fphp-workflow/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wol-soft","download_url":"https://codeload.github.com/wol-soft/php-workflow/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247694882,"owners_count":20980733,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["error-handling","modularization","php","php74","php8","reusable-components","workflow","workflow-engine"],"created_at":"2024-09-24T20:04:49.285Z","updated_at":"2025-04-09T20:21:13.025Z","avatar_url":"https://github.com/wol-soft.png","language":"PHP","readme":"[![Latest Version](https://img.shields.io/packagist/v/wol-soft/php-workflow.svg)](https://packagist.org/packages/wol-soft/php-workflow)\n[![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%207.4-8892BF.svg)](https://php.net/)\n[![Maintainability](https://api.codeclimate.com/v1/badges/a7c6d1c276d2a6aba61e/maintainability)](https://codeclimate.com/github/wol-soft/php-workflow/maintainability)\n[![Build Status](https://github.com/wol-soft/php-workflow/actions/workflows/main.yml/badge.svg)](https://github.com/wol-soft/php-workflow/actions/workflows/main.yml)\n[![Coverage Status](https://coveralls.io/repos/github/wol-soft/php-workflow/badge.svg)](https://coveralls.io/github/wol-soft/php-workflow)\n[![MIT License](https://img.shields.io/packagist/l/wol-soft/php-workflow.svg)](https://github.com/wol-soft/php-workflow/blob/master/LICENSE)\n\n# php-workflow\n\nCreate controlled workflows from small pieces.\n\nThis library provides a predefined set of stages to glue together your workflows.\nYou implement small self-contained pieces of code and define the execution order - everything else will be done by the execution control.\n\nBonus: you will get an execution log for each executed workflow - if you want to see what's happening.\n\n## Table of Contents ##\n\n* [Workflow vs. process](#Workflow-vs-process)\n* [Installation](#Installation)\n* [Example workflow](#Example-workflow)\n* [Workflow container](#Workflow-container)\n* [Stages](#Stages)\n* [Workflow control](#Workflow-control)\n* [Nested workflows](#Nested-workflows)\n* [Loops](#Loops)\n* [Step dependencies](#Step-dependencies)\n  * [Required container values](#Required-container-values)\n* [Error handling, logging and debugging](#Error-handling-logging-and-debugging)\n  * [Custom output formatter](#Custom-output-formatter)\n* [Tests](#Tests)\n* [Workshop](#Workshop)\n\n## Workflow vs. process\n\nBefore we start to look at coding with the library let's have a look, what a workflow implemented with this library can and what a workflow can't.\n\nLet's assume we want to sell an item via an online shop.\nIf a customer purchases an item he walks through the process of purchasing an item.\nThis process contains multiple steps.\nEach process step can be represented by a workflow implemented with this library. For example:\n\n* Customer registration\n* Add items to the basket\n* Checkout the basket\n* ...\n\nThis library helps you to implement the process steps in a structured way.\nIt doesn't control the process flow.\n\nNow we know which use cases this library aims at. Now let's install the library and start coding.\n\n## Installation\n\nThe recommended way to install php-workflow is through [Composer](http://getcomposer.org):\n\n```\n$ composer require wol-soft/php-workflow\n```\n\nRequirements of the library:\n\n- Requires at least PHP 7.4\n\n## Example workflow\n\nLet's have a look at a code example first.\nOur example will represent the code to add a song to a playlist in a media player.\nCasually you will have a controller method which glues together all necessary steps with many if's, returns, try-catch blocks and so on.\nNow let's have a look at a possible workflow definition:\n\n```php\n$workflowResult = (new \\PHPWorkflow\\Workflow('AddSongToPlaylist'))\n    -\u003evalidate(new CurrentUserIsAllowedToEditPlaylistValidator())\n    -\u003evalidate(new PlaylistAlreadyContainsSongValidator())\n    -\u003ebefore(new AcceptOpenSuggestionForSong())\n    -\u003eprocess(new AddSongToPlaylist())\n    -\u003eonSuccess(new NotifySubscribers())\n    -\u003eonSuccess(new AddPlaylistLogEntry())\n    -\u003eonSuccess(new UpdateLastAddedToPlaylists())\n    -\u003eonError(new RecoverLog())\n    -\u003eexecuteWorkflow();\n```\n\nThis workflow may create an execution log which looks like the following (more examples coming up later):\n\n```\nProcess log for workflow 'AddSongToPlaylist':\nValidation:\n  - Check if the playlist is editable: ok\n  - Check if the playlist already contains the requested song: ok\nBefore:\n  - Accept open suggestions for songs which shall be added: skipped (No open suggestions for playlist)\nProcess:\n  - Add the songs to the playlist: ok\n    - Appended song at the end of the playlist\n    - New playlist length: 2\nOn Success:\n  - Notify playlist subscribers about added song: ok\n    - Notified 5 users\n  - Persist changes in the playlist log: ok\n  - Update the users list of last contributed playlists: ok\n\nSummary:\n  - Workflow execution: ok\n    - Execution time: 45.27205ms\n```\n\nNow let's check what exactly happens.\nEach step of your workflow is represented by an own class which implements the step.\nSteps may be used in multiple workflows (for example the **CurrentUserIsAllowedToEditPlaylistValidator** can be used in every workflow which modifies playlists).\nEach of these classes representing a single step must implement the **\\PHPWorkflow\\Step\\WorkflowStep** interface.\nUntil you call the **executeWorkflow** method no step will be executed.\n\nBy calling the **executeWorkflow** method the workflow engine is triggered to start the execution with the first used stage.\nIn our example the validations will be executed first.\nIf all validations are successfully the next stage will be executed otherwise the workflow execution will be cancelled.\n\nLet's have a more precise look at the implementation of a single step through the example of the *before* step **AcceptOpenSuggestionForSong**.\nSome feature background to understand what's happening in our example: our application allows users to suggest songs for playlists.\nIf the owner of a playlist adds a song to a playlist which already exists as an open suggestion the suggestion shall be accepted instead of adding the song to the playlist and leave the suggestion untouched.\nNow let's face the implementation with some inline comments to describe the workflow control:\n\n```php\nclass AcceptOpenSuggestionForSong implements \\PHPWorkflow\\Step\\WorkflowStep {\n    /**\n     * Each step must provide a description. The description will be used in the debug\n     * log of the workflow to get a readable representation of an executed workflow \n     */\n    public function getDescription(): string\n    {\n        return 'Accept open suggestions for songs which shall be added to a playlist';\n    }\n\n    /**\n     * Each step will get access to two objects to interact with the workflow.\n     * First the WorkflowControl object $control which provides methods to skip\n     * steps, mark tests as failed, add debug information etc.\n     * Second the WorkflowContainer object $container which allows us to get access\n     * to various workflow related objects.\n     */\n    public function run(\n        \\PHPWorkflow\\WorkflowControl $control,\n        \\PHPWorkflow\\State\\WorkflowContainer $container\n    ) {\n        $openSuggestions = (new SuggestionRepository())\n            -\u003egetOpenSuggestionsByPlaylistId($container-\u003eget('playlist')-\u003egetId());\n\n        // If we detect a condition which makes a further execution of the step\n        // unnecessary we can simply skip the further execution.\n        // By providing a meaningful reason our workflow debug log will be helpful.\n        if (empty($openSuggestions)) {\n            $control-\u003eskipStep('No open suggestions for playlist');\n        }\n\n        foreach ($openSuggestions as $suggestion) {\n            if ($suggestion-\u003egetSongId() === $container-\u003eget('song')-\u003egetId()) {\n                if ((new SuggestionService())-\u003eacceptSuggestion($suggestion)) {\n                    // If we detect a condition where the further workflow execution is\n                    // unnecessary we can skip the further execution.\n                    // In this example the open suggestion was accepted successfully so\n                    // the song must not be added to the playlist via the workflow.\n                    $control-\u003eskipWorkflow('Accepted open suggestion');\n                }\n\n                // We can add warnings to the debug log. Another option in this case could\n                // be to call $control-\u003efailWorkflow() if we want the workflow to fail in\n                // an error case.\n                // In our example, if the suggestion can't be accepted, we want to add the\n                // song to the playlist via the workflow.\n                $control-\u003ewarning(\"Failed to accept open suggestion {$suggestion-\u003egetId()}\");\n            }\n        }\n\n        // for completing the debug log we mark this step as skipped if no action has been\n        // performed. If we don't mark the step as skipped and no action has been performed\n        // the step will occur as 'ok' in the debug log - depends on your preferences :)\n        $control-\u003eskipStep('No matching open suggestion');\n    }\n}\n```\n\n## Workflow container\n\nNow let's have a more detailed look at the **WorkflowContainer** which helps us, to share data and objects between our workflow steps.\nThe relevant objects for our example workflow is the **User** who wants to add the song, the **Song** object of the song to add and the **Playlist** object.\nBefore we execute our workflow we can set up a **WorkflowContainer** which contains all relevant objects:\n\n```php\n$workflowContainer = (new \\PHPWorkflow\\State\\WorkflowContainer())\n    -\u003eset('user', Session::getUser())\n    -\u003eset('song', (new SongRepository())-\u003egetSongById($request-\u003eget('songId')))\n    -\u003eset('playlist', (new PlaylistRepository())-\u003egetPlaylistById($request-\u003eget('playlistId')));\n```\n\nThe workflow container provides the following interface:\n\n```php\n// returns an item or null if the key doesn't exist\npublic function get(string $key)\n// set or update a value\npublic function set(string $key, $value): self\n// remove an entry\npublic function unset(string $key): self\n// check if a key exists\npublic function has(string $key): bool\n```\n\nEach workflow step may define requirements, which entries must be present in the workflow container before the step is executed.\nFor more details have a look at [Required container values](#Required-container-values).\n\nAlternatively to set and get the values from the **WorkflowContainer** via string keys you can extend the **WorkflowContainer** and add typed properties/functions to handle values in a type-safe manner:\n\n```php\nclass AddSongToPlaylistWorkflowContainer extends \\PHPWorkflow\\State\\WorkflowContainer {\n    public function __construct(\n        public User $user,\n        public Song $song,\n        public Playlist $playlist,\n    ) {}\n}\n\n$workflowContainer = new AddSongToPlaylistWorkflowContainer(\n    Session::getUser(),\n    (new SongRepository())-\u003egetSongById($request-\u003eget('songId')),\n    (new PlaylistRepository())-\u003egetPlaylistById($request-\u003eget('playlistId')),\n);\n```\n\nWhen we execute the workflow via **executeWorkflow** we can inject the **WorkflowContainer**.\n\n```php\n$workflowResult = (new \\PHPWorkflow\\Workflow('AddSongToPlaylist'))\n    // ...\n    -\u003eexecuteWorkflow($workflowContainer);\n```\n\nAnother possibility would be to define a step in the **Prepare** stage (e.g. **PopulateAddSongToPlaylistContainer**) which populates the automatically injected empty **WorkflowContainer** object.\n\n## Stages\n\nThe following predefined stages are available when defining a workflow:\n\n* Prepare\n* Validate\n* Before\n* Process\n* OnSuccess\n* OnError\n* After\n\nEach stage has a defined set of stages which may be called afterwards (e.g. you may skip the **Before** stage).\nWhen setting up a workflow your IDE will support you by suggesting only possible next steps via autocompletion.\nEach workflow must contain at least one step in the **Process** stage.\n\nAny step added to the workflow may throw an exception. Each exception will be caught and is handled like a failed step.\nIf a step in the stages **Prepare**, **Validate** (see details for the stage) or **Before** fails, the workflow is failed and will not be executed further.\n\nAny step may skip or fail the workflow via the **WorkflowControl**.\nIf the **Process** stage has been executed and any later step tries to fail or skip the whole workflow it's handled as a failed/skipped step.\n\nNow let's have a look at some stage-specific details.\n\n### Prepare\n\nThis stage allows you to add steps which must be executed before any validation or process execution is triggered.\nSteps may contain data loading, gaining workflow relevant semaphores, etc.\n\n### Validate\n\nThis stage allows you to execute validations.\nThere are two types of validations: hard validations and soft validations.\nAll hard validations of the workflow will be executed before the soft validations.\nIf a hard validation fails the workflow will be stopped immediately (e.g. access right violations).\nAll soft validations of the workflow will be executed independently of their result.\nAll failing soft validations will be collected in a **\\PHPWorkflow\\Exception\\WorkflowValidationException** which is thrown at the end of the validation stage if any of the soft validations failed.\n\nWhen you attach a validation to your workflow the second parameter of the **validate** method defines if the validation is a soft or a hard validation:\n\n```php\n\n$workflowResult = (new \\PHPWorkflow\\Workflow('AddSongToPlaylist'))\n    // hard validator: if the user isn't allowed to edit the playlist\n    // the workflow execution will be cancelled immediately\n    -\u003evalidate(new CurrentUserIsAllowedToEditPlaylistValidator(), true)\n    // soft validators: all validators will be executed\n    -\u003evalidate(new PlaylistAlreadyContainsSongValidator())\n    -\u003evalidate(new SongGenreMatchesPlaylistGenreValidator())\n    -\u003evalidate(new PlaylistContainsNoSongsFromInterpret())\n    // ...\n```\n\nIn the provided example any of the soft validators may fail (e.g. the **SongGenreMatchesPlaylistGenreValidator** checks if the genre of the song matches the playlist, the **PlaylistContainsNoSongsFromInterpret** may check for duplicated interprets).\nThe thrown **WorkflowValidationException** allows us to determine all violations and set up a corresponding error message.\nIf all validators pass the next stage will be executed.\n\n### Before\n\nThis stage allows you to perform preparatory steps with the knowledge that the workflow execution is valid.\nThis steps may contain the allocation of resources, filtering the data to process etc.\n\n### Process\n\nThis stage contains your main logic of the workflow. If any of the steps fails no further steps of the process stage will be executed.\n\n### OnSuccess\n\nThis stage allows you to define steps which shall be executed if all steps of the **Process** stage are executed successfully.\nFor example logging, notifications, sending emails, etc.\n\nAll steps of the stage will be executed, even if some steps fail. All failing steps will be reported as warnings.\n\n### OnError\n\nThis stage allows you to define steps which shall be executed if any step of the **Process** stage fails.\nFor example logging, setting up recovery data, etc.\n\nAll steps of the stage will be executed, even if some steps fail. All failing steps will be reported as warnings.\n\n### After\n\nThis stage allows you to perform cleanup steps after all other stages have been executed. The steps will be executed regardless of the successful execution of the **Process** stage.\n\nAll steps of the stage will be executed, even if some steps fail. All failing steps will be reported as warnings.\n\n## Workflow control\n\nThe **WorkflowControl** object which is injected into each step provides the following interface to interact with the workflow:\n\n```php\n// Mark the current step as skipped.\n// Use this if you detect, that the step execution is not necessary\n// (e.g. disabled by config, no entity to process, ...)\npublic function skipStep(string $reason): void;\n\n// Mark the current step as failed. A failed step before and during the processing of\n// a workflow leads to a failed workflow.\npublic function failStep(string $reason): void;\n\n// Mark the workflow as failed. If the workflow is failed after the process stage has\n// been executed it's handled like a failed step.\npublic function failWorkflow(string $reason): void;\n\n// Skip the further workflow execution (e.g. if you detect it's not necessary to process\n// the workflow). If the workflow is skipped after the process stage has been executed\n// it's handled like a skipped step.\npublic function skipWorkflow(string $reason): void;\n\n// Useful when using loops to cancel the current iteration (all upcoming steps).\n// If used outside a loop, it behaves like skipStep.\npublic function continue(string $reason): void;\n\n// Useful when using loops to break the loop (all upcoming steps and iterations).\n// If used outside a loop, it behaves like skipStep.\npublic function break(string $reason): void;\n\n// Attach any additional debug info to your current step.\n// The infos will be shown in the workflow debug log.\npublic function attachStepInfo(string $info): void\n\n// Add a warning to the workflow.\n// All warnings will be collected and shown in the workflow debug log.\n// You can provide an additional exception which caused the warning.\n// If you provide the exception, exception details will be added to the debug log.\npublic function warning(string $message, ?Exception $exception = null): void;\n```\n\n## Nested workflows\n\nIf some of your steps become more complex you may want to have a look into the `NestedWorkflow` wrapper which allows you to use a second workflow as a step of your workflow:\n\n```php\n$parentWorkflowContainer = (new \\PHPWorkflow\\State\\WorkflowContainer())-\u003eset('parent-data', 'Hello');\n$nestedWorkflowContainer = (new \\PHPWorkflow\\State\\WorkflowContainer())-\u003eset('nested-data', 'World');\n\n$workflowResult = (new \\PHPWorkflow\\Workflow('AddSongToPlaylist'))\n    -\u003evalidate(new CurrentUserIsAllowedToEditPlaylistValidator())\n    -\u003ebefore(new \\PHPWorkflow\\Step\\NestedWorkflow(\n        (new \\PHPWorkflow\\Workflow('AcceptOpenSuggestions'))\n            -\u003evalidate(new PlaylistAcceptsSuggestionsValidator())\n            -\u003ebefore(new LoadOpenSuggestions())\n            -\u003eprocess(new AcceptOpenSuggestions())\n            -\u003eonSuccess(new NotifySuggestor()),\n        $nestedWorkflowContainer,\n    ))\n    -\u003eprocess(new AddSongToPlaylist())\n    -\u003eonSuccess(new NotifySubscribers())\n    -\u003eexecuteWorkflow($parentWorkflowContainer);\n```\n\nEach nested workflow must be executable (contain at least one **Process** step).\n\nThe debug log of your nested workflow will be embedded in the debug log of your main workflow.\n\nAs you can see in the example you can inject a dedicated **WorkflowContainer** into the nested workflow.\nThe nested workflow will gain access to a merged **WorkflowContainer** which provides all data and methods of your main workflow container and your nested container.\nIf you add additional data to the merged container the data will be present in your main workflow container after the nested workflow execution has been completed.\nFor example your implementations of the steps used in the nested workflow will have access to the keys `nested-data` and `parent-data`.\n\n## Loops\n\nIf you handle multiple entities in your workflows at once you may need loops.\nAn approach would be to set up a single step which contains the loop and all logic which is required to be executed in a loop.\nBut if there are multiple steps required to be executed in the loop you may want to split the step into various steps.\nBy using the `Loop` class you can execute multiple steps in a loop.\nFor example let's assume our `AddSongToPlaylist` becomes a `AddSongsToPlaylist` workflow which can add multiple songs at once:\n\n```php\n$workflowResult = (new \\PHPWorkflow\\Workflow('AddSongToPlaylist'))\n    -\u003evalidate(new CurrentUserIsAllowedToEditPlaylistValidator())\n    -\u003eprocess(\n        (new \\PHPWorkflow\\Step\\Loop(new SongLoop()))\n            -\u003eaddStep(new AddSongToPlaylist())\n            -\u003eaddStep(new ClearSongCache())\n    )\n    -\u003eonSuccess(new NotifySubscribers())\n    -\u003eexecuteWorkflow($workflowContainer);\n```\n\nOur process step now implements a loop controlled by the `SongLoop` class.\nThe loop contains our two steps `AddSongToPlaylist` and `ClearSongCache`.\nThe implementation of the `SongLoop` class must implement the `PHPWorkflow\\Step\\LoopControl` interface.\nLet's have a look at an example implementation:\n\n```php\nclass SongLoop implements \\PHPWorkflow\\Step\\LoopControl {\n    /**\n     * As well as each step also each loop must provide a description.\n     */\n    public function getDescription(): string\n    {\n        return 'Loop over all provided songs';\n    }\n\n    /**\n     * This method will be called before each loop run.\n     * $iteration will contain the current iteration (0 on first run etc)\n     * You have access to the WorkflowControl and the WorkflowContainer.\n     * If the method returns true the next iteration will be executed.\n     * Otherwise the loop is completed.\n     */\n    public function executeNextIteration(\n        int $iteration,\n        \\PHPWorkflow\\WorkflowControl $control,\n        \\PHPWorkflow\\State\\WorkflowContainer $container\n    ): bool {\n        // all songs handled - end the loop\n        if ($iteration === count($container-\u003eget('songs'))) {\n            return false;\n        }\n\n        // add the current song to the container so the steps\n        // of the loop can access the entry\n        $container-\u003eset('currentSong', $container-\u003eget('songs')[$iteration]);\n\n        return true;\n    }\n}\n```\n\nA loop step may contain a nested workflow if you need more complex steps.\n\nTo control the flow of the loop from the steps you can use the `continue` and `break` methods on the `WorkflowControl` object.\n\nBy default, a loop is stopped if a step fails.\nYou can set the second parameter of the `Loop` class (`$continueOnError`) to true to continue the execution with the next iteration.\nIf you enable this option a failed step will not result in a failed workflow.\nInstead, a warning will be added to the process log.\nCalls to `failWorkflow` and `skipWorkflow` will always cancel the loop (and consequently the workflow) independent of the option.\n\n## Step dependencies\n\nEach step implementation may apply dependencies to the step.\nBy defining dependencies you can set up validation rules which are checked before your step is executed (for example: which data must be provided in the workflow  container).\nIf any of the dependencies is not fulfilled, the step will not be executed and is handled as a failed step.\n\nNote: as this feature uses [Attributes](https://www.php.net/manual/de/language.attributes.overview.php), it is only available if you use PHP \u003e= 8.0.\n\n### Required container values\n\nWith the `\\PHPWorkflow\\Step\\Dependency\\Required` attribute you can define keys which must be present in the provided workflow container.\nThe keys consequently must be provided in the initial workflow or be populated by a previous step.\nAdditionally to the key you can also provide the type of the value (eg. `string`).\n\nTo define the dependency you simply annotate the provided workflow container parameter:\n\n```php\npublic function run(\n    \\PHPWorkflow\\WorkflowControl $control,\n    // The key customerId must contain a string\n    #[\\PHPWorkflow\\Step\\Dependency\\Required('customerId', 'string')]\n    // The customerAge must contain an integer. But also null is accepted.\n    // Each type definition can be prefixed with a ? to accept null.\n    #[\\PHPWorkflow\\Step\\Dependency\\Required('customerAge', '?int')]\n    // Objects can also be type hinted\n    #[\\PHPWorkflow\\Step\\Dependency\\Required('created', \\DateTime::class)]\n    \\PHPWorkflow\\State\\WorkflowContainer $container,\n) {\n    // Implementation which can rely on the defined keys to be present in the container.\n}\n```\n\nThe following types are supported: `string`, `bool`, `int`, `float`, `object`, `array`, `iterable`, `scalar` as well as object type hints by providing the corresponding FQCN.\n\n## Error handling, logging and debugging\n\nThe **executeWorkflow** method returns an **WorkflowResult** object which provides the following methods to determine the result of the workflow:\n\n```php\n// check if the workflow execution was successful\npublic function success(): bool;\n// check if warnings were emitted during the workflow execution\npublic function hasWarnings(): bool;\n// get a list of warnings, grouped by stage\npublic function getWarnings(): array;\n// get the exception which caused the workflow to fail\npublic function getException(): ?Exception;\n// get the debug execution log of the workflow\npublic function debug(?OutputFormat $formatter = null);\n// access the container which was used for the workflow\npublic function getContainer(): WorkflowContainer;\n// get the last executed step\n// (e.g. useful to determine which step caused a workflow to fail)\npublic function getLastStep(): WorkflowStep;\n```\n\nAs stated above workflows with failing steps before the **Process** stage will be aborted, otherwise the **Process** stage and all downstream stages will be executed.\n\nBy default, the execution of a workflow throws an exception if an error occurs.\nThe thrown exception will be a **\\PHPWorkflow\\Exception\\WorkflowException** which allows you to access the **WorkflowResult** object via the **getWorkflowResult** method.\n\nThe **debug** method provides an execution log including all processed steps with their status, attached data  as well as a list of all warnings and performance data.\n\nSome example outputs for our example workflow may look like the following.\n\n#### Successful execution\n\n```\nProcess log for workflow 'AddSongToPlaylist':\nValidation:\n  - Check if the playlist is editable: ok\n  - Check if the playlist already contains the requested song: ok\nBefore:\n  - Accept open suggestions for songs which shall be added: skipped (No open suggestions for playlist)\nProcess:\n  - Add the songs to the playlist: ok\n    - Appended song at the end of the playlist\n    - New playlist length: 2\nOn Success:\n  - Notify playlist subscribers about added song: ok\n    - Notified 5 users\n  - Persist changes in the playlist log: ok\n  - Update the users list of last contributed playlists: ok\n\nSummary:\n  - Workflow execution: ok\n    - Execution time: 45.27205ms\n```\n\nNote the additional data added to the debug log for the **Process** stage and the **NotifySubscribers** step via the **attachStepInfo** method of the **WorkflowControl**.\n\n#### Failed workflow\n\n```\nProcess log for workflow 'AddSongToPlaylist':\nValidation:\n  - Check if the playlist is editable: failed (playlist locked)\n\nSummary:\n  - Workflow execution: failed\n    - Execution time: 6.28195ms\n```\n\nIn this example the **CurrentUserIsAllowedToEditPlaylistValidator** step threw an exception with the message `playlist locked`.\n\n#### Workflow skipped\n\n```\nProcess log for workflow 'AddSongToPlaylist':\nValidation:\n  - Check if the playlist is editable: ok\n  - Check if the playlist already contains the requested song: ok\nBefore:\n  - Accept open suggestions for songs which shall be added: ok (Accepted open suggestion)\n\nSummary:\n  - Workflow execution: skipped (Accepted open suggestion)\n    - Execution time: 89.56986ms\n```\n\nIn this example the **AcceptOpenSuggestionForSong** step found a matching open suggestion and successfully accepted the suggestion.\nConsequently, the further workflow execution is skipped.\n\n### Custom output formatter\n\nThe output of the `debug` method can be controlled via an implementation of the `OutputFormat` interface.\nBy default a string representation of the execution will be returned (just like the example outputs).\n\nCurrently the following additional formatters are implemented:\n\n| Formatter       | Description   |\n| --------------- | ------------- |\n| `StringLog`     | The default formatter. Creates a string representation. \u003cbr /\u003eExample:\u003cbr /\u003e`$result-\u003edebug();` |\n| `WorkflowGraph` | Creates a SVG file containing a graph which represents the workflow execution. The generated image will be stored in the provided target directory. Requires `dot` executable.\u003cbr /\u003eExample:\u003cbr /\u003e`$result-\u003edebug(new WorkflowGraph('/var/log/workflow/graph'));` |\n| `GraphViz`      | Returns a string containing [GraphViz](https://graphviz.org/) code for a graph representing the workflow execution.  \u003cbr /\u003eExample:\u003cbr /\u003e`$result-\u003edebug(new GraphViz());`|\n\n## Tests\n\nThe library is tested via [PHPUnit](https://phpunit.de/).\n\nAfter installing the dependencies of the library via `composer update` you can execute the tests with `./vendor/bin/phpunit` (Linux) or `vendor\\bin\\phpunit.bat` (Windows).\nThe test names are optimized for the usage of the `--testdox` output.\n\nIf you want to test workflows you may include the `PHPWorkflow\\Tests\\WorkflowTestTrait` which adds methods to simplify asserting workflow results.\nThe following methods are added to your test classes:\n\n```php\n// assert the debug output of the workflow. See library tests for example usages\nprotected function assertDebugLog(string $expected, WorkflowResult $result): void\n// provide a step which you expect to fail the workflow.\n// example: $this-\u003eexpectFailAtStep(MyFailingStep::class, $workflowResult);\nprotected function expectFailAtStep(string $step, WorkflowResult $result): void\n// provide a step which you expect to skip the workflow.\n// example: $this-\u003eexpectSkipAtStep(MySkippingStep::class, $workflowResult);\nprotected function expectSkipAtStep(string $step, WorkflowResult $result): void\n```\n\n## Workshop\n\nMaybe you want to try out the library and lack a simple idea to solve with the library.\nTherefore, here's a small workshop which covers most of the library's features.\nImplement the task given below (which, to be fair, can surely be implemented easier without the library but the library is designed to support large workflows with a lot of business logic) to have an idea how coding with the library works.\n\nYour data input for this task is a simple array with a list of persons in the following format:\n\n```php\n[\n    'firstname' =\u003e string,\n    'lastname' =\u003e string,\n    'age' =\u003e int,\n]\n```\n\nThe workflow shall implement the following steps:\n\n1. Check if the list is empty. In this case finish the workflow directly\n2. Check if the list contains persons with an age below 18 years. In this case the workflow should fail\n3. Make sure each firstname and lastname is populated. If any empty fields are detected, the workflow should fail\n4. Before processing the list normalize the firstnames and lastnames (`ucfirst` and `trim`)\n   - Make sure the workflow log contains the amount of changed data sets\n5. Process the list. The processing itself splits up into the following steps:\n   1. Make sure a directory of your choice contains a CSV file for each age from the input data\n      - If the file doesn't exist, create a new file\n      - The workflow log must contain information about new files\n   2. Add all persons from your input data to the corresponding files\n      - The workflow log must display the amount of persons added to each file\n6. If all persons were persisted successfully create a ZIP backup of all files\n7. If an error occurred rollback to the last existing ZIP backup\n\nIf you have finished implementing the workflow, pick a step of your choice and implement a unit test for the step.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwol-soft%2Fphp-workflow","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwol-soft%2Fphp-workflow","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwol-soft%2Fphp-workflow/lists"}