notwarp / laravel-workflow
将 Symfony Workflow 组件集成到 Laravel 中。
资助包维护!
lucaterribili
Requires
- php: ^7.4|^8.0|^8.1
- illuminate/console: ^8.0|^9.0|^10.0
- illuminate/contracts: ^8.0|^9.0|^10.0
- illuminate/support: ^8.0|^9.0|^10.0
- symfony/event-dispatcher-contracts: ^2.4
- symfony/process: ^6.0
- symfony/workflow: ^6.0
Requires (Dev)
- fakerphp/faker: ^1.13
- mockery/mockery: ^1.2
- orchestra/testbench: ^6.0|^7.0
- phpunit/phpunit: ^9.0
- symfony/contracts: ^2.3
- symfony/var-dumper: ^6.0
- dev-develop
- 8.1.x-dev
- v6.0.3
- v6.0.2.x-dev
- v6.0.2
- v6.0.1
- v5.0.5
- v5.0.4
- v5.0.3
- v5.0.2
- v5.0.1
- v5.0.0
- v4.0.3
- v4.0.2
- v4.0.1
- v4.0.0
- dev-master / 3.x-dev
- v3.3.3
- v3.3.2
- v3.3.1
- v3.3.0
- v3.2.2
- v3.2.1
- v3.2.0
- v3.1.2
- v3.1.1
- v3.1.0
- v3.0.1
- v3.0.0
- v2.1.0
- v2.0.4
- v2.0.2
- v2.0.1
- v2.0.0
- v1.2.3
- 1.0.4
- 1.0.3
- 1.0.2
- 1.0.1
- 1.0.0
- dev-laravel-10
- dev-dependabot/github_actions/stefanzweifel/git-auto-commit-action-5
- dev-dependabot/github_actions/actions/checkout-4
- dev-ciccio
- dev-custom_event
- dev-fix_props
- dev-laravel_9
This package is auto-updated.
Last update: 2024-09-28 14:46:35 UTC
README
这是从 zerodahero/laravel-workflow 分支出来的。我对这个包的需求比其他包能够维护的更前沿。对 brexis 为原始工作和适应性所做的贡献表示巨大的敬意。
在 Laravel 中使用 Symfony Workflow 组件
安装
composer require lucaterribili/laravel-workflow
Laravel 支持
从 v3 升级到 v4
更改是对 PHP 和 Laravel 版本的更改,本版本仅支持 PHP 8.0、8.1 和 Laravel 9。如果您需要使用旧版本,请从 3.4 版本开始。
从 v2 升级到 v3
从 v2 升级到 v3 的最大变化是依赖项。为了匹配 Symfony v5 组件,Laravel 版本提升到 v7。如果您使用的是 Laravel v6 或更早版本,应继续使用此包的 v2 版本。
为了匹配 Symfony v5 工作流组件的更改,"arguments" 配置选项已更改为 "property"。这描述了工作流关联的模型上的属性(在大多数情况下,您可以简单地将 "arguments" 键名更改为 "property",并将其设置为字符串而不是之前的数组)。
此外,"initial_place" 键已更改为 "initial_places",以与 Symfony 组件保持一致。
非包发现
如果您不使用包发现
在 config/app.php
中的 providers 数组中添加 ServiceProvider
<?php 'providers' => [ ... LucaTerribili\LaravelWorkflow\WorkflowServiceProvider::class, ]
将 Workflow
门面添加到您的 facades 数组中
<?php ... 'Workflow' => LucaTerribili\LaravelWorkflow\Facades\WorkflowFacade::class,
配置
Laravel v < 9.0
发布配置文件
php artisan vendor:publish --provider="LucaTerribili\LaravelWorkflow\WorkflowServiceProvider"
在 config/workflow.php
中配置您的流程
<?php // Full workflow, annotated. return [ // Name of the workflow is the key 'straight' => [ 'type' => 'workflow', // or 'state_machine', defaults to 'workflow' if omitted // The marking store can be omitted, and will default to 'multiple_state' // for workflow and 'single_state' for state_machine if the type is omitted 'marking_store' => [ 'property' => 'marking', // this is the property on the model, defaults to 'marking' 'class' => MethodMarkingStore::class, // optional, uses EloquentMethodMarkingStore by default (for Eloquent models) ], // optional top-level metadata 'metadata' => [ // any data ], 'supports' => ['App\BlogPost'], // objects this workflow supports // Specifies events to dispatch (only in 'workflow', not 'state_machine') // - set `null` to dispatch all events (default, if omitted) // - set to empty array (`[]`) to dispatch no events // - set to array of events to dispatch only specific events // Note that announce will dispatch a guard event on the next transition // (if announce isn't dispatched the next transition won't guard until checked/applied) 'events_to_dispatch' => [ Symfony\Component\Workflow\WorkflowEvents::ENTER, Symfony\Component\Workflow\WorkflowEvents::LEAVE, Symfony\Component\Workflow\WorkflowEvents::TRANSITION, Symfony\Component\Workflow\WorkflowEvents::ENTERED, Symfony\Component\Workflow\WorkflowEvents::COMPLETED, Symfony\Component\Workflow\WorkflowEvents::ANNOUNCE, ], 'places' => ['draft', 'review', 'rejected', 'published'], 'initial_places' => ['draft'], // defaults to the first place if omitted 'transitions' => [ 'to_review' => [ 'from' => 'draft', 'to' => 'review', // optional transition-level metadata 'metadata' => [ // any data ] ], 'publish' => [ 'from' => 'review', 'to' => 'published' ], 'reject' => [ 'from' => 'review', 'to' => 'rejected' ] ], ] ];
更简洁的设置(适用于 eloquent 模型的流程)。
<?php // Simple workflow. Sets type 'workflow', with a 'multiple_state' workflow // on the 'marking' property of any 'App\BlogPost' model. return [ 'simple' => [ 'supports' => ['App\BlogPost'], // objects this workflow supports 'places' => ['draft', 'review', 'rejected', 'published'], 'transitions' => [ 'to_review' => [ 'from' => 'draft', 'to' => 'review' ], 'publish' => [ 'from' => 'review', 'to' => 'published' ], 'reject' => [ 'from' => 'review', 'to' => 'rejected' ] ], ] ];
如果您使用的是 "multiple_state" 类型的流程(即在您的流程中同时处于多个位置),则需要将支持类/Eloquent 模型中的标记转换为数组。有关更多信息,请参阅 Laravel 文档。
您还可以添加元数据,类似于 Symfony 实现(注意:它不是以与 Symfony 实现相同的方式收集的,但应该可以正常工作。如果这不是这种情况,请提交拉取请求或问题。)
<?php return [ 'straight' => [ 'type' => 'workflow', // or 'state_machine' 'metadata' => [ 'title' => 'Blog Publishing Workflow', ], 'supports' => ['App\BlogPost'], 'places' => [ 'draft' => [ 'metadata' => [ 'max_num_of_words' => 500, ] ], 'review', 'rejected', 'published' ], 'transitions' => [ 'to_review' => [ 'from' => 'draft', 'to' => 'review', 'metadata' => [ 'priority' => 0.5, ] ], 'publish' => [ 'from' => 'review', 'to' => 'published' ], 'reject' => [ 'from' => 'review', 'to' => 'rejected' ] ], ] ];
Laravel v >= 9.*
从 Laravel 9 开始,我们不使用配置。您需要将工作流程存储在数据库中。我们有两个表:Workflow 和 Transitions
模型在包内,但您可以覆盖这些更改配置文件
<?php return [ 'models' => [ 'workflow' => LucaTerribili\LaravelWorkflow\Models\Workflow::class, 'transition' => LucaTerribili\LaravelWorkflow\Models\Transition::class, ], ];
Workflow 表的记录示例
$workflows = array( array('id' => '1','name' => 'MacroTicket a progetto','supports' => '["App\\\\Models\\\\MacroTicketProject"]','places' => '[{"name": "unplannable", "sort": 0, "label": "Non pianificabile"}, {"name": "waiting_plane", "sort": 1, "label": "In attesa pianificazione"}, {"name": "new_plane", "sort": 2, "label": "Da ripianificare"}, {"name": "waiting_plane_accept", "sort": 3, "label": "In attesa accettazione pianificazione"}, {"name": "planned", "sort": 4, "label": "Pianificato"}, {"name": "approved", "sort": 5, "label": "Approvato"}, {"name": "bonded", "sort": 6, "label": "Vincolato"}, {"name": "partial_migrated", "sort": 7, "label": "Migrato parziale"}, {"name": "tested", "sort": 8, "label": "Collaudato"}, {"name": "deleted", "sort": 9, "label": "Annullato"}]','start_place' => 'unplannable','final_place' => 'tested','last_places' => '["tested", "deleted"]','created_at' => '2022-03-07 11:17:29','updated_at' => '2022-03-07 11:17:29') );
Transitions 表的记录示例
$transitions = array( array('id' => '1','workflow_id' => '1','name' => 'to_waiting_plane','label' => 'Pianifica','from' => '["unplannable"]','to' => 'waiting_plane','permission' => 'be.workflow.macro_ticket.waiting_plane','created_at' => '2022-03-07 11:17:29','updated_at' => '2022-03-07 11:17:29'), array('id' => '2','workflow_id' => '1','name' => 'to_ask_approved','label' => 'Manda in approvazione','from' => '["waiting_plane", "new_plane"]','to' => 'waiting_plane_accept','permission' => 'be.workflow.macro_ticket.waiting_plane_accept','created_at' => '2022-03-07 11:17:29','updated_at' => '2022-03-07 11:17:29'), array('id' => '3','workflow_id' => '1','name' => 'to_replane','label' => 'Rifiuta','from' => '["waiting_plane_accept"]','to' => 'new_plane','permission' => 'be.workflow.macro_ticket.to_replane','created_at' => '2022-03-07 11:17:29','updated_at' => '2022-03-07 11:17:29'), array('id' => '4','workflow_id' => '1','name' => 'to_planned','label' => 'Approva pianificazione','from' => '["waiting_plane_accept"]','to' => 'planned','permission' => 'be.workflow.macro_ticket.to_planned','created_at' => '2022-03-07 11:17:29','updated_at' => '2022-03-07 11:17:29'), array('id' => '5','workflow_id' => '1','name' => 'to_reject','label' => 'Anulla pianificazione','from' => '["planned"]','to' => 'new_plane','permission' => 'be.workflow.macro_ticket.to_reject','created_at' => '2022-03-07 11:17:29','updated_at' => '2022-03-07 11:17:29'), array('id' => '6','workflow_id' => '1','name' => 'to_approved','label' => 'Approva intervento','from' => '["planned"]','to' => 'approved','permission' => 'be.workflow.macro_ticket.to_approved','created_at' => '2022-03-07 11:17:29','updated_at' => '2022-03-07 11:17:29'), array('id' => '7','workflow_id' => '1','name' => 'to_bonded','label' => 'Vincola','from' => '["approved"]','to' => 'bonded','permission' => 'be.workflow.macro_ticket.to_bonded','created_at' => '2022-03-07 11:17:29','updated_at' => '2022-03-07 11:17:29'), array('id' => '8','workflow_id' => '1','name' => 'from_bonded_to_replane','label' => 'Rimuovi vincoli','from' => '["bonded"]','to' => 'new_plane','permission' => 'be.workflow.macro_ticket.to_replane','created_at' => '2022-03-07 11:17:29','updated_at' => '2022-03-07 11:17:29'), array('id' => '9','workflow_id' => '1','name' => 'delete','label' => 'Annulla','from' => '["unplannable", "waiting_plane", "new_plane", "waiting_plane_accept", "planned", "approved", "bonded", "partial_migrated", "tested"]','to' => 'deleted','permission' => 'be.workflow.macro_ticket.delete','created_at' => '2022-03-07 11:17:29','updated_at' => '2022-03-07 11:17:29'), array('id' => '10','workflow_id' => '1','name' => 'to_tested','label' => 'Collauda','from' => '["approved"]','to' => 'tested','permission' => 'be.workflow.macro_ticket.to_tested','created_at' => '2022-03-07 11:17:29','updated_at' => '2022-03-07 11:17:29') );
在支持类中使用 WorkflowTrait
<?php namespace App; use Illuminate\Database\Eloquent\Model; use LucaTerribili\LaravelWorkflow\Traits\WorkflowTrait; class BlogPost extends Model { use WorkflowTrait; }
用法
<?php use App\BlogPost; use Workflow; $post = BlogPost::find(1); $workflow = Workflow::get($post); // if more than one workflow is defined for the BlogPost class $workflow = Workflow::get($post, $workflowName); // or get it directly from the trait $workflow = $post->workflow_get(); // if more than one workflow is defined for the BlogPost class $workflow = $post->workflow_get($workflowName); $workflow->can($post, 'publish'); // False $workflow->can($post, 'to_review'); // True $transitions = $workflow->getEnabledTransitions($post); // Apply a transition $workflow->apply($post, 'to_review'); $post->save(); // Don't forget to persist the state // Get the workflow directly // Using the WorkflowTrait $post->workflow_can('publish'); // True $post->workflow_can('to_review'); // False // Get the post transitions foreach ($post->workflow_transitions() as $transition) { echo $transition->getName(); } // Apply a transition $post->workflow_apply('publish'); $post->save();
Symfony Workflow 用法
一旦有了底层的 Symfony 工作流组件,您就可以像在 Symfony 中一样做任何您想做的事情。以下提供了一些示例,但请务必查看 Symfony 文档 以更好地理解这里的情况。
<?php use App\Blogpost; use Workflow; $post = BlogPost::find(1); $workflow = $post->workflow_get(); // Get the current places $places = $workflow->getMarking($post)->getPlaces(); // Get the definition $definition = $workflow->getDefinition(); // Get the metadata $metadata = $workflow->getMetadataStore(); // or get a specific piece of metadata $workflowMetadata = $workflow->getMetadataStore()->getWorkflowMetadata(); $placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata($place); // string place name $transitionMetadata = $workflow->getMetadataStore()->getTransitionMetadata($transition); // transition object // or by key $otherPlaceMetadata = $workflow->getMetadataStore()->getMetadata('max_num_of_words', 'draft');
使用事件
此包提供了一组在转换期间触发的事件
LucaTerribili\LaravelWorkflow\Events\Guard LucaTerribili\LaravelWorkflow\Events\Leave LucaTerribili\LaravelWorkflow\Events\Transition LucaTerribili\LaravelWorkflow\Events\Enter LucaTerribili\LaravelWorkflow\Events\Entered
建议您使用 Symfony的点语法风格事件发射,因为这提供了最佳的精度来监听事件,并防止接收同一事件类多次的“相同”事件。工作流组件在每个工作流事件中调度多个事件,如果仅按类名监听,这可能导致“重复”事件的监听。
注意:这些事件在3.1.1版本之前接收Symfony事件,从3.1.1版本开始将接收此包的事件。
<?php namespace App\Listeners; use LucaTerribili\LaravelWorkflow\Events\GuardEvent; class BlogPostWorkflowSubscriber { // ... /** * Register the listeners for the subscriber. * * @param Illuminate\Events\Dispatcher $events */ public function subscribe($events) { // can use any of the three formats: // workflow.guard // workflow.[workflow name].guard // workflow.[workflow name].guard.[transition name] $events->listen( 'workflow.straight.guard', 'App\Listeners\BlogPostWorkflowSubscriber@onGuard' ); // workflow.leave // workflow.[workflow name].leave // workflow.[workflow name].leave.[place name] $events->listen( 'workflow.straight.leave', 'App\Listeners\BlogPostWorkflowSubscriber@onLeave' ); // workflow.transition // workflow.[workflow name].transition // workflow.[workflow name].transition.[transition name] $events->listen( 'workflow.straight.transition', 'App\Listeners\BlogPostWorkflowSubscriber@onTransition' ); // workflow.enter // workflow.[workflow name].enter // workflow.[workflow name].enter.[place name] $events->listen( 'workflow.straight.enter', 'App\Listeners\BlogPostWorkflowSubscriber@onEnter' ); // workflow.entered // workflow.[workflow name].entered // workflow.[workflow name].entered.[place name] $events->listen( 'workflow.straight.entered', 'App\Listeners\BlogPostWorkflowSubscriber@onEntered' ); // workflow.completed // workflow.[workflow name].completed // workflow.[workflow name].completed.[transition name] $events->listen( 'workflow.straight.completed', 'App\Listeners\BlogPostWorkflowSubscriber@onCompleted' ); // workflow.announce // workflow.[workflow name].announce // workflow.[workflow name].announce.[transition name] $events->listen( 'workflow.straight.announce', 'App\Listeners\BlogPostWorkflowSubscriber@onAnnounce' ); } }
您也可以使用更典型的Laravel风格来订阅事件,尽管这已不再推荐,因为它可能导致根据您的事件监听方式而产生“重复”事件。
<?php namespace App\Listeners; use LucaTerribili\LaravelWorkflow\Events\GuardEvent; class BlogPostWorkflowSubscriber { /** * Handle workflow guard events. */ public function onGuard(GuardEvent $event) { /** Symfony\Component\Workflow\Event\GuardEvent */ $originalEvent = $event->getOriginalEvent(); /** @var App\BlogPost $post */ $post = $originalEvent->getSubject(); $title = $post->title; if (empty($title)) { // Posts with no title should not be allowed $originalEvent->setBlocked(true); } } /** * Handle workflow leave event. */ public function onLeave($event) { // The event can also proxy to the original event $subject = $event->getSubject(); // is the same as: $subject = $event->getOriginalEvent()->getSubject(); } /** * Handle workflow transition event. */ public function onTransition($event) {} /** * Handle workflow enter event. */ public function onEnter($event) {} /** * Handle workflow entered event. */ public function onEntered($event) {} /** * Register the listeners for the subscriber. * * @param Illuminate\Events\Dispatcher $events */ public function subscribe($events) { $events->listen( 'LucaTerribili\LaravelWorkflow\Events\GuardEvent', 'App\Listeners\BlogPostWorkflowSubscriber@onGuard' ); $events->listen( 'LucaTerribili\LaravelWorkflow\Events\LeaveEvent', 'App\Listeners\BlogPostWorkflowSubscriber@onLeave' ); $events->listen( 'LucaTerribili\LaravelWorkflow\Events\TransitionEvent', 'App\Listeners\BlogPostWorkflowSubscriber@onTransition' ); $events->listen( 'LucaTerribili\LaravelWorkflow\Events\EnterEvent', 'App\Listeners\BlogPostWorkflowSubscriber@onEnter' ); $events->listen( 'LucaTerribili\LaravelWorkflow\Events\EnteredEvent', 'App\Listeners\BlogPostWorkflowSubscriber@onEntered' ); } }
工作流与状态机
当使用多状态工作流时,有必要区分一个地方可以转换到多个位置的情况,或者一个主题在正好多个位置转换到同一个地方的情况。由于配置是PHP数组,您必须将后者的情况“嵌套”到数组中,以便使用位置数组构建转换,而不是通过单个位置进行循环。
示例1.正好两个位置转换到一个位置
在此示例中,草稿必须同时处于content_approved
和legal_approved
状态。
<?php return [ 'straight' => [ 'type' => 'workflow', 'metadata' => [ 'title' => 'Blog Publishing Workflow', ], 'marking_store' => [ 'property' => 'currentPlace' ], 'supports' => ['App\BlogPost'], 'places' => [ 'draft', 'content_review', 'content_approved', 'legal_review', 'legal_approved', 'published' ], 'transitions' => [ 'to_review' => [ 'from' => 'draft', 'to' => ['content_review', 'legal_review'], ], // ... transitions to "approved" states here 'publish' => [ 'from' => [ // note array in array ['content_review', 'legal_review'] ], 'to' => 'published' ], // ... ], ] ];
示例2.任一两个位置转换到一个位置
在此示例中,草稿可以从content_approved
或legal_approved
转换到published
。
<?php return [ 'straight' => [ 'type' => 'workflow', 'metadata' => [ 'title' => 'Blog Publishing Workflow', ], 'marking_store' => [ 'property' => 'currentPlace' ], 'supports' => ['App\BlogPost'], 'places' => [ 'draft', 'content_review', 'content_approved', 'legal_review', 'legal_approved', 'published' ], 'transitions' => [ 'to_review' => [ 'from' => 'draft', 'to' => ['content_review', 'legal_review'], ], // ... transitions to "approved" states here 'publish' => [ 'from' => [ 'content_review', 'legal_review' ], 'to' => 'published' ], // ... ], ] ];
输出工作流
Symfony工作流使用GraphvizDumper创建工作流图像。您可能需要安装Graphviz的dot
命令。
php artisan workflow:dump workflow_name --class App\\BlogPost
您可以使用--format
选项更改图像格式。默认格式是png。
php artisan workflow:dump workflow_name --format=jpg
如果您想将输出保存到根目录以外的目录,可以使用--disk
和--path
选项来设置存储磁盘(默认为local
)和路径(默认为root_path()
)。
php artisan workflow:dump workflow-name --class=App\\BlogPost --disk=s3 --path="workflows/diagrams/"
使用跟踪模式
如果您通过某种动态方式(可能是通过数据库)加载工作流定义,您可能希望启用注册跟踪。这将使您能够看到已加载的内容,以防止或忽略重复的工作流定义。
在workflow_registry.php
配置文件中将track_loaded
设置为true
。
<?php return [ /** * When set to true, the registry will track the workflows that have been loaded. * This is useful when you're loading from a DB, or just loading outside of the * main config files. */ 'track_loaded' => false, /** * Only used when track_loaded = true * * When set to true, a registering a duplicate workflow will be ignored (will not load the new definition) * When set to false, a duplicate workflow will throw a DuplicateWorkflowException */ 'ignore_duplicates' => false, ];
您可以使用工作流注册表上的addFromArray
方法动态加载工作流。
<?php /** * Load the workflow type definition into the registry */ protected function loadWorkflow() { $registry = app()->make('workflow'); $workflowName = 'straight'; $workflowDefinition = [ // Workflow definition here // (same format as config/symfony docs) // This should be the definition only, // not including the key for the name. // See note below on initial_places for an example. ]; $registry->addFromArray($workflowName, $workflowDefinition); // or if catching duplicates try { $registry->addFromArray($workflowName, $workflowDefinition); } catch (DuplicateWorkflowException $e) { // already loaded } }
注意:动态工作流没有持久性,此包假设您以某种方式存储了这些内容(数据库等)。要使用动态工作流,您需要在使用它之前加载工作流。上面的loadWorkflow
方法可以与模型的boot
或类似方法相关联。
您还可以在工作流定义中指定initial_places
,如果它不是“位置”列表中的第一个位置。
<?php return [ 'type' => 'workflow', // or 'state_machine' 'metadata' => [ 'title' => 'Blog Publishing Workflow', ], 'marking_store' => [ 'property' => 'currentPlace' ], 'supports' => ['App\BlogPost'], 'places' => [ 'review', 'rejected', 'published', 'draft', => [ 'metadata' => [ 'max_num_of_words' => 500, ] ] ], 'initial_places' => 'draft', // or set to an array if multiple initial places 'transitions' => [ 'to_review' => [ 'from' => 'draft', 'to' => 'review', 'metadata' => [ 'priority' => 0.5, ] ], 'publish' => [ 'from' => 'review', 'to' => 'published' ], 'reject' => [ 'from' => 'review', 'to' => 'rejected' ] ], ];