1package Workflow; 2 3use warnings; 4use strict; 5use v5.14.0; # warnings 6use base qw( Workflow::Base ); 7use Log::Log4perl qw( get_logger ); 8use Workflow::Context; 9use Workflow::Exception qw( workflow_error ); 10use Exception::Class; 11use Workflow::Factory qw( FACTORY ); 12use Carp qw(croak carp); 13use English qw( -no_match_vars ); 14 15my @FIELDS = qw( id type description state last_update time_zone ); 16my @INTERNAL = qw( _factory _observers ); 17__PACKAGE__->mk_accessors( @FIELDS, @INTERNAL ); 18 19$Workflow::VERSION = '1.59'; 20 21use constant NO_CHANGE_VALUE => 'NOCHANGE'; 22 23######################################## 24# INTERNAL METHODS 25 26sub add_observer { 27 my ($self, @observers) = @_; 28 29 if (not $self->_observers) { 30 $self->_observers( [] ); 31 } 32 push @{$self->_observers}, @observers; 33 34 return; 35} 36 37sub notify_observers { 38 my ($self, @args) = @_; 39 40 return unless $self->_observers; 41 $_->($self, @args) for @{$self->_observers}; 42 43 return; 44} 45 46######################################## 47# PUBLIC METHODS 48 49# this is our only read-write property... 50 51sub context { 52 my ( $self, $context ) = @_; 53 if ($context) { 54 55 # We already have a context, merge the new one with ours; (the 56 # new one wins with dupes) 57 58 if ( $self->{context} ) { 59 $self->{context}->merge($context); 60 } else { 61 $context->param( workflow_id => $self->id ); 62 $self->{context} = $context; 63 } 64 } 65 unless ( $self->{context} ) { 66 $self->{context} = Workflow::Context->new(); 67 } 68 return $self->{context}; 69} 70 71sub get_current_actions { 72 my ( $self, $group ) = @_; 73 $self->log->debug( "Getting current actions for wf '", $self->id, "'" ); 74 my $wf_state = $self->_get_workflow_state; 75 return $wf_state->get_available_action_names( $self, $group ); 76} 77 78sub get_action { 79 my ( $self, $action_name ) = @_; 80 81 my $state = $self->state; 82 $self->log->debug( 83 "Trying to find action '$action_name' in state '$state'"); 84 85 my $wf_state = $self->_get_workflow_state; 86 unless ( $wf_state->contains_action($action_name) ) { 87 workflow_error 88 "State '$state' does not contain action '$action_name'"; 89 } 90 $self->log->debug("Action '$action_name' exists in state '$state'"); 91 92 my $action = $self->_get_workflow_state()->get_action( $self, $action_name ); 93 94 # This will throw an exception which we want to bubble up 95 $wf_state->evaluate_action( $self, $action_name ); 96 return $action; 97} 98 99sub get_action_fields { 100 my ( $self, $action_name ) = @_; 101 my $action = $self->get_action($action_name); 102 return $action->fields; 103} 104 105sub execute_action { 106 my ( $self, $action_name, $autorun ) = @_; 107 108 # This checks the conditions behind the scenes, so there's no 109 # explicit 'check conditions' step here 110 111 my $action = $self->get_action($action_name); 112 113 # Need this in case we encounter an exception after we store the 114 # new state 115 116 my $old_state = $self->state; 117 my ( $new_state, $action_return ); 118 119 eval { 120 $action->validate($self); 121 $self->log->debug("Action validated ok"); 122 $action_return = $action->execute($self); 123 $self->log->debug("Action executed ok"); 124 125 $new_state = $self->_get_next_state( $action_name, $action_return ); 126 if ( $new_state ne NO_CHANGE_VALUE ) { 127 $self->log->info("Set new state '$new_state' after action executed"); 128 $self->state($new_state); 129 } 130 131 # this will save the workflow histories as well as modify the 132 # state of the workflow history to reflect the NEW state of 133 # the workflow; if it fails we should have some means for the 134 # factory to rollback other transactions... 135 136 # Update 137 # Jim Brandt 4/16/2008: Implemented transactions for DBI persisters. 138 # Implementation still depends on each persister. 139 140 $self->_factory()->save_workflow($self); 141 142 # If using a DBI persister with no autocommit, commit here. 143 $self->_factory()->_commit_transaction($self); 144 145 $self->log->info("Saved workflow with possible new state ok"); 146 }; 147 148 # If there's an exception, reset the state to the original one and 149 # rethrow 150 151 if ($EVAL_ERROR) { 152 my $error = $EVAL_ERROR; 153 $self->log->error( 154 "Caught exception from action: $error; reset ", 155 "workflow to old state '$old_state'" 156 ); 157 $self->state($old_state); 158 159 $self->_factory()->_rollback_transaction($self); 160 161 # If it is a validation error we rethrow it so it can be evaluated 162 # by the caller to provide better feedback to the user 163 if (Exception::Class->caught('Workflow::Exception::Validation')) { 164 $EVAL_ERROR->rethrow(); 165 } 166 167 # Don't use 'workflow_error' here since $error should already 168 # be a Workflow::Exception object or subclass 169 170 croak $error; 171 } 172 173 # clear condition cache on state change 174 delete $self->{'_condition_result_cache'}; 175 $self->notify_observers( 'execute', $old_state, $action_name, $autorun ); 176 177 my $new_state_obj = $self->_get_workflow_state; 178 if ( $old_state ne $new_state ) { 179 $self->notify_observers( 'state change', $old_state, $action_name, 180 $autorun ); 181 } 182 183 if ( $new_state_obj->autorun ) { 184 $self->log->info( 185 "State '$new_state' marked to be run ", 186 "automatically; executing that state/action..." 187 ); 188 $self->_auto_execute_state($new_state_obj); 189 } 190 return $self->state; 191} 192 193sub add_history { 194 my ( $self, @items ) = @_; 195 196 my @to_add = (); 197 foreach my $item (@items) { 198 if ( ref $item eq 'HASH' ) { 199 $item->{workflow_id} = $self->id; 200 $item->{time_zone} = $self->time_zone(); 201 push @to_add, Workflow::History->new($item); 202 $self->log->debug("Adding history from hashref"); 203 } elsif ( ref $item and $item->isa('Workflow::History') ) { 204 $item->workflow_id( $self->id ); 205 push @to_add, $item; 206 $self->log->debug("Adding history object directly"); 207 } else { 208 workflow_error "I don't know how to add a history of ", "type '", 209 ref($item), "'"; 210 } 211 212 if ($EVAL_ERROR) { 213 workflow_error "Unable to assert history object"; 214 } 215 } 216 push @{ $self->{_histories} }, @to_add; 217 $self->notify_observers( 'add history', \@to_add ); 218} 219 220sub get_history { 221 my ($self) = @_; 222 $self->{_histories} ||= []; 223 my @uniq_history = (); 224 my %seen_ids = (); 225 my @all_history = ( 226 $self->_factory()->get_workflow_history($self), 227 @{ $self->{_histories} } 228 ); 229 foreach my $history (@all_history) { 230 my $id = $history->id; 231 if ($id) { 232 unless ( $seen_ids{$id} ) { 233 push @uniq_history, $history; 234 } 235 $seen_ids{$id}++; 236 } else { 237 push @uniq_history, $history; 238 } 239 } 240 return @uniq_history; 241} 242 243sub get_unsaved_history { 244 my ($self) = @_; 245 return grep { !$_->is_saved } @{ $self->{_histories} }; 246} 247 248sub clear_history { 249 my ($self) = @_; 250 $self->{_histories} = []; 251} 252 253######################################## 254# PRIVATE METHODS 255 256sub init { 257 my ( $self, $id, $current_state, $config, $wf_state_objects, $factory ) 258 = @_; 259 $id ||= ''; 260 $factory ||= FACTORY; 261 $self->log->info( 262 "Instantiating workflow of with ID '$id' and type ", 263 "'$config->{type}' with current state '$current_state'" 264 ); 265 266 $self->id($id) if ($id); 267 $self->_factory($factory); 268 269 $self->state($current_state); 270 $self->type( $config->{type} ); 271 $self->description( $config->{description} ); 272 my $time_zone 273 = exists $config->{time_zone} ? $config->{time_zone} : 'floating'; 274 $self->time_zone($time_zone); 275 276 # other properties go into 'param'... 277 while ( my ( $key, $value ) = each %{$config} ) { 278 next if ( $key =~ /^(type|description)$/ ); 279 next if ( ref $value ); 280 $self->log->debug("Assigning parameter '$key' -> '$value'"); 281 $self->param( $key, $value ); 282 } 283 284 # Now set all the Workflow::State objects created and cached by the 285 # factory 286 287 foreach my $wf_state ( @{$wf_state_objects} ) { 288 $self->_set_workflow_state($wf_state); 289 } 290} 291 292# Override from Class::Accessor so only certain callers can set 293# properties 294 295sub set { 296 my ( $self, $prop, $value ) = @_; 297 my $calling_pkg = ( caller 1 )[0]; 298 unless ( $calling_pkg =~ /^Workflow/ ) { 299 carp "Tried to set from: ", join ', ', caller 1; 300 workflow_error 301 "Don't try to use my private setters from '$calling_pkg'!"; 302 } 303 $self->{$prop} = $value; 304} 305 306sub _get_action { # for backward compatibility with 1.49 and before 307 goto &get_action; 308} 309 310sub _get_workflow_state { 311 my ( $self, $state ) = @_; 312 $state ||= ''; # get rid of -w... 313 my $use_state = $state || $self->state; 314 $self->log->debug( 315 "Finding Workflow::State object for state [given: $use_state] ", 316 "[internal: ", $self->state, "]" ); 317 my $wf_state = $self->{_states}{$use_state}; 318 unless ($wf_state) { 319 workflow_error "No state '$use_state' exists in workflow '", 320 $self->type, "'"; 321 } 322 return $wf_state; 323} 324 325sub _set_workflow_state { 326 my ( $self, $wf_state ) = @_; 327 $self->{_states}{ $wf_state->state } = $wf_state; 328} 329 330sub _get_next_state { 331 my ( $self, $action_name, $action_return ) = @_; 332 my $wf_state = $self->_get_workflow_state; 333 return $wf_state->get_next_state( $action_name, $action_return ); 334} 335 336sub _auto_execute_state { 337 my ( $self, $wf_state ) = @_; 338 my $action_name; 339 eval { $action_name = $wf_state->get_autorun_action_name($self); }; 340 if ($EVAL_ERROR) 341 { # we found an error, possibly more than one or none action 342 # are available in this state 343 if ( !$wf_state->may_stop() ) { 344 345 # we are in autorun, but stopping is not allowed, so 346 # rethrow 347 my $error = $EVAL_ERROR; 348 $error->rethrow(); 349 } 350 } else { # everything is fine, execute action 351 $self->log->debug( 352 "Found action '$action_name' to execute in ", 353 "autorun state ", 354 $wf_state->state 355 ); 356 $self->execute_action( $action_name, 1 ); 357 } 358} 359 3601; 361 362__END__ 363 364=pod 365 366=begin markdown 367 368[![CPAN version](https://badge.fury.io/pl/Workflow.svg)](http://badge.fury.io/pl/Workflow) 369[![Build Status](https://travis-ci.org/jonasbn/perl-workflow.svg?branch=master)](https://travis-ci.org/jonasbn/perl-workflow) 370[![Coverage Status](https://coveralls.io/repos/github/jonasbn/perl-workflow/badge.svg?branch=master)](https://coveralls.io/github/jonasbn/perl-workflow?branch=master) 371 372=end markdown 373 374=head1 NAME 375 376Workflow - Simple, flexible system to implement workflows 377 378=head1 VERSION 379 380This documentation describes version 1.59 of Workflow 381 382=head1 SYNOPSIS 383 384 use Workflow::Factory qw( FACTORY ); 385 386 # Defines a workflow of type 'myworkflow' 387 my $workflow_conf = 'workflow.xml'; 388 389 # contents of 'workflow.xml' 390 391 <workflow> 392 <type>myworkflow</type> 393 <time_zone>local</time_zone> 394 <description>This is my workflow.</description> 395 396 <state name="INITIAL"> 397 <action name="upload file" resulting_state="uploaded" /> 398 </state> 399 <state name="uploaded" autorun="yes"> 400 <action name="verify file" resulting_state="verified file"> 401 <!-- everyone other than 'CWINTERS' must verify --> 402 <condition test="$context->{user} ne 'CWINTERS'" /> 403 </action> 404 <action name="null" resulting_state="annotated"> 405 <condition test="$context->{user} eq 'CWINTERS'" /> 406 </action> 407 </state> 408 <state name="verified file"> 409 <action name="annotate"> 410 <condition name="can_annotate" /> 411 </action> 412 <action name="null"> 413 <condition name="!can_annotate" /> 414 </action> 415 </state> 416 <state name="annotated" autorun="yes" may_stop="yes"> 417 <action name="null" resulting_state="finished"> 418 <condition name="completed" /> 419 </action> 420 </state> 421 <state name="finished" /> 422 </workflow> 423 424 # Defines actions available to the workflow 425 my $action_conf = 'action.xml'; 426 427 # contents of 'action.xml' 428 429 <actions> 430 <action name="upload file" class="MyApp::Action::Upload"> 431 <field name="path" label="File Path" 432 description="Path to file" is_required="yes" /> 433 </action> 434 435 <action name="verify file" class="MyApp::Action::Verify"> 436 <validator name="filesize_cap"> 437 <arg>$file_size</arg> 438 </validator> 439 </action> 440 441 <action name="annotate" class="MyApp::Action::Annotate" /> 442 443 <action name="null" class="Workflow::Action::Null" /> 444 </actions> 445 446 # Defines conditions available to the workflow 447 my $condition_conf = 'condition.xml'; 448 449 # contents of 'condition.xml' 450 451 <conditions> 452 <condition name="can_annotate" 453 class="MyApp::Condition::CanAnnotate" /> 454 </conditions> 455 456 # Defines validators available to the actions 457 my $validator_conf = 'validator.xml'; 458 459 # contents of 'validator.xml' 460 461 <validators> 462 <validator name="filesize_cap" class="MyApp::Validator::FileSizeCap"> 463 <param name="max_size" value="20M" /> 464 </validator> 465 </validators> 466 467 # Stock the factory with the configurations; we can add more later if 468 # we want 469 $self->_factory()->add_config_from_file( 470 workflow => $workflow_conf, 471 action => $action_conf, 472 condition => $condition_conf, 473 validator => $validator_conf 474 ); 475 476 # Instantiate a new workflow... 477 my $workflow = $self->_factory()->create_workflow( 'myworkflow' ); 478 print "Workflow ", $workflow->id, " ", 479 "currently at state ", $workflow->state, "\n"; 480 481 # Display available actions... 482 print "Available actions: ", $workflow->get_current_actions, "\n"; 483 484 # Get the data needed for action 'upload file' (assumed to be 485 # available in the current state) and display the fieldname and 486 # description 487 488 print "Action 'upload file' requires the following fields:\n"; 489 foreach my $field ( $workflow->get_action_fields( 'FOO' ) ) { 490 print $field->name, ": ", $field->description, 491 "(Required? ", $field->is_required, ")\n"; 492 } 493 494 # Add data to the workflow context for the validators, conditions and 495 # actions to work with 496 497 my $context = $workflow->context; 498 $context->param( current_user => $user ); 499 $context->param( sections => \@sections ); 500 $context->param( path => $path_to_file ); 501 502 # Execute one of them 503 $workflow->execute_action( 'upload file' ); 504 505 print "New state: ", $workflow->state, "\n"; 506 507 # Later.... fetch an existing workflow 508 my $id = get_workflow_id_from_user( ... ); 509 my $workflow = $self->_factory()->fetch_workflow( 'myworkflow', $id ); 510 print "Current state: ", $workflow->state, "\n"; 511 512=head1 QUICK START 513 514The F<eg/ticket/> directory contains a configured workflow system. 515You can access the same data and logic in two ways: 516 517=over 518 519=item * a command-line application (ticket.pl) 520 521=item * a CGI script (ticket.cgi) 522 523=item * a web application (ticket_web.pl) 524 525=back 526 527To initialize: 528 529 perl ticket.pl --db 530 531To run the command-line application: 532 533 perl ticket.pl 534 535To access the database and data from CGI, add the relevant 536configuration for your web server and call ticket.cgi: 537 538 http://www.mysite.com/workflow/ticket.cgi 539 540To start up the standalone web server: 541 542 perl ticket_web.pl 543 544(Barring changes to HTTP::Daemon and forking the standalone server 545won't work on Win32; use CGI instead, although patches are always 546welcome.) 547 548For more info, see F<eg/ticket/README> 549 550=head1 DESCRIPTION 551 552=head2 Overview 553 554This is a standalone workflow system. It is designed to fit into your 555system rather than force your system to fit to it. You can save 556workflow information to a database or the filesystem (or a custom 557storage). The different components of a workflow system can be 558included separately as libraries to allow for maximum reusibility. 559 560=head2 User Point of View 561 562As a user you only see two components, plus a third which is really 563embedded into another: 564 565=over 4 566 567=item * 568 569L<Workflow::Factory> - The factory is your interface for creating new 570workflows and fetching existing ones. You also feed all the necessary 571configuration files and/or data structures to the factory to 572initialize it. 573 574=item * 575 576L<Workflow> - When you get the workflow object from the workflow 577factory you can only use it in a few ways -- asking for the current 578state, actions available for the state, data required for a particular 579action, and most importantly, executing a particular action. Executing 580an action is how you change from one state to another. 581 582=item * 583 584L<Workflow::Context> - This is a blackboard for data from your 585application to the workflow system and back again. Each instantiation 586of a L<Workflow> has its own context, and actions executed by the 587workflow can read data from and deposit data into the context. 588 589=back 590 591=head2 Developer Point of View 592 593The workflow system has four basic components: 594 595=over 4 596 597=item * 598 599B<workflow> - The workflow is a collection of states; you define the 600states, how to move from one state to another, and under what 601conditions you can change states. 602 603This is represented by the L<Workflow> object. You normally do not 604need to subclass this object for customization. 605 606=item * 607 608B<action> - The action is defined by you or in a separate library. The 609action is triggered by moving from one state to another and has access 610to the workflow and more importantly its context. 611 612The base class for actions is the L<Workflow::Action> class. 613 614=item * 615 616B<condition> - Within the workflow you can attach one or more 617conditions to an action. These ensure that actions only get executed 618when certain conditions are met. Conditions are completely arbitrary: 619typically they will ensure the user has particular access rights, but 620you can also specify that an action can only be executed at certain 621times of the day, or from certain IP addresses, and so forth. Each 622condition is created once at startup then passed a context to check 623every time an action is checked to see if it can be executed. 624 625The base class for conditions is the L<Workflow::Condition> class. 626 627=item * 628 629B<validator> - An action can specify one or more validators to ensure 630that the data available to the action is correct. The data to check 631can be as simple or complicated as you like. Each validator is created 632once then passed a context and data to check every time an action is 633executed. 634 635The base class for validators is the L<Workflow::Validator> class. 636 637=back 638 639=head1 WORKFLOW BASICS 640 641=head2 Just a Bunch of States 642 643A workflow is just a bunch of states with rules on how to move between 644them. These are known as transitions and are triggered by some sort of 645event. A state is just a description of object properties. You can 646describe a surprisingly large number of processes as a series of 647states and actions to move between them. The application shipped with 648this distribution uses a fairly common application to illustrate: the 649trouble ticket. 650 651When you create a workflow you have one action available to you: 652create a new ticket ('create issue'). The workflow has a state 653'INITIAL' when it is first created, but this is just a bootstrapping 654exercise since the workflow must always be in some state. 655 656The workflow action 'create issue' has a property 'resulting_state', 657which just means: if you execute me properly the workflow will be in 658the new state 'CREATED'. 659 660All this talk of 'states' and 'transitions' can be confusing, but just 661match them to what happens in real life -- you move from one action to 662another and at each step ask: what happens next? 663 664You create a trouble ticket: what happens next? Anyone can add 665comments to it and attach files to it while administrators can edit it 666and developers can start working on it. Adding comments does not 667really change what the ticket is, it just adds 668information. Attachments are the same, as is the admin editing the 669ticket. 670 671But when someone starts work on the ticket, that is a different 672matter. When someone starts work they change the answer to: what 673happens next? Whenever the answer to that question changes, that means 674the workflow has changed state. 675 676=head2 Discover Information from the Workflow 677 678In addition to declaring what the resulting state will be from an 679action the action also has a number of 'field' properties that 680describe that data it required to properly execute it. 681 682This is an example of discoverability. This workflow system is setup 683so you can ask it what you can do next as well as what is required to 684move on. So to use our ticket example we can do this, creating the 685workflow and asking it what actions we can execute right now: 686 687 my $wf = Workflow::$self->_factory()->create_workflow( 'Ticket' ); 688 my @actions = $wf->get_current_actions; 689 690We can also interrogate the workflow about what fields are necessary 691to execute a particular action: 692 693 print "To execute the action 'create issue' you must provide:\n\n"; 694 my @fields = $wf->get_action_fields( 'create issue' ); 695 foreach my $field ( @fields ) { 696 print $field->name, " (Required? ", $field->is_required, ")\n", 697 $field->description, "\n\n"; 698 } 699 700=head2 Provide Information to the Workflow 701 702To allow the workflow to run into multiple environments we must have a 703common way to move data between your application, the workflow and the 704code that moves it from one state to another. 705 706Whenever the L<Workflow::Factory> creates a new workflow it associates 707the workflow with a L<Workflow::Context> object. The context is what 708moves the data from your application to the workflow and the workflow 709actions. 710 711For instance, the workflow has no idea what the 'current user' is. Not 712only is it unaware from an application standpoint but it does not 713presume to know where to get this information. So you need to tell it, 714and you do so through the context. 715 716The fact that the workflow system proscribes very little means it can 717be used in lots of different applications and interfaces. If a system 718is too closely tied to an interface (like the web) then you have to 719create some potentially ugly hacks to create a more convenient avenue 720for input to your system (such as an e-mail approving a document). 721 722The L<Workflow::Context> object is extremely simple to use -- you ask 723a workflow for its context and just get/set parameters on it: 724 725 # Get the username from the Apache object 726 my $username = $r->connection->user; 727 728 # ...set it in the context 729 $wf->context->param( user => $username ); 730 731 # somewhere else you'll need the username: 732 733 $news_object->{created_by} = $wf->context->param( 'user' ); 734 735=head2 Controlling What Gets Executed 736 737A typical process for executing an action is: 738 739=over 4 740 741=item * 742 743Get data from the user 744 745=item * 746 747Fetch a workflow 748 749=item * 750 751Set the data from the user to the workflow context 752 753=item * 754 755Execute an action on the context 756 757=back 758 759When you execute the action a number of checks occur. The action needs 760to ensure: 761 762=over 4 763 764=item * 765 766The data presented to it are valid -- date formats, etc. This is done 767with a validator, more at L<Workflow::Validator> 768 769=item * 770 771The environment meets certain conditions -- user is an administrator, 772etc. This is done with a condition, more at L<Workflow::Condition> 773 774=back 775 776Once the action passes these checks and successfully executes we 777update the permanent workflow storage with the new state, as long as 778the application has declared it. 779 780=head1 WORKFLOWS ARE OBSERVABLE 781 782=head2 Purpose 783 784It's useful to have your workflow generate events so that other parts 785of a system can see what's going on and react. For instance, say you 786have a new user creation process. You want to email the records of all 787users who have a first name of 'Sinead' because you're looking for 788your long-lost sister named 'Sinead'. You'd create an observer class 789like: 790 791 package FindSinead; 792 793 sub update { 794 my ( $class, $wf, $event, $new_state ) = @_; 795 return unless ( $event eq 'state change' ); 796 return unless ( $new_state eq 'CREATED' ); 797 my $context = $wf->context; 798 return unless ( $context->param( 'first_name' ) eq 'Sinead' ); 799 800 my $user = $context->param( 'user' ); 801 my $username = $user->username; 802 my $email = $user->email; 803 my $mailer = get_mailer( ... ); 804 $mailer->send( 'foo@bar.com','Found her!', 805 "We found Sinead under '$username' at '$email' ); 806 } 807 808And then associate it with your workflow: 809 810 <workflow> 811 <type>SomeFlow</type> 812 <observer class="FindSinead" /> 813 ... 814 815Every time you create/fetch a workflow the associated observers are 816attached to it. 817 818=head2 Events Generated 819 820You can attach listeners to workflows and catch events at a few points 821in the workflow lifecycle; these are the events fired: 822 823=over 4 824 825=item * 826 827B<create> - Issued after a workflow is first created. 828 829No additional parameters. 830 831=item * 832 833B<fetch> - Issued after a workflow is fetched from the persister. 834 835No additional parameters. 836 837=item * 838 839B<save> - Issued after a workflow is successfully saved. 840 841No additional parameters. 842 843=item * 844 845B<execute> - Issued after a workflow is successfully executed and 846saved. 847 848Adds the parameters C<$old_state>, C<$action_name> and C<$autorun>. 849C<$old_state> includes the state of the workflow before the action 850was executed, C<$action_name> is the action name that was executed and 851C<$autorun> is set to 1 if the action just executed was started 852using autorun. 853 854=item * 855 856B<state change> - Issued after a workflow is successfully executed, 857saved and results in a state change. The event will not be fired if 858you executed an action that did not result in a state change. 859 860Adds the parameters C<$old_state>, C<$action> and C<$autorun>. 861C<$old_state> includes the state of the workflow before the action 862was executed, C<$action> is the action name that was executed and 863C<$autorun> is set to 1 if the action just executed was autorun. 864 865=item * 866 867B<add history> - Issued after one or more history objects added to a 868workflow object. 869 870The additional argument is an arrayref of all L<Workflow::History> 871objects added to the workflow. (Note that these will not be persisted 872until the workflow is persisted.) 873 874=back 875 876=head2 Configuring 877 878You configure the observers directly in the 'workflow' configuration 879item. Each 'observer' may have either a 'class' or 'sub' entry within 880it that defines the observer's location. 881 882We load these classes at startup time. So if you specify an observer 883that doesn't exist you see the error when the workflow system is 884initialized rather than the system tries to use the observer. 885 886For instance, the following defines two observers: 887 888 <workflow> 889 <type>ObservedItem</type> 890 <description>This is...</description> 891 892 <observer class="SomeObserver" /> 893 <observer sub="SomeOtherObserver::Functions::other_sub" /> 894 895In the first declaration we specify the class ('SomeObserver') that 896will catch observations using its C<update()> method. In the second 897we're naming exactly the subroutine ('other_sub()' in the class 898'SomeOtherObserver::Functions') that will catch observations. 899 900All configured observers get all events. It's up to each observer to 901figure out what it wants to handle. 902 903=head1 WORKFLOW METHODS 904 905The following documentation is for the workflow object itself rather 906than the entire system. 907 908=head2 Object Methods 909 910=head3 execute_action( $action_name, $autorun ) 911 912Execute the action C<$action_name>. Typically this changes the state 913of the workflow. If C<$action_name> is not in the current state, fails 914one of the conditions on the action, or fails one of the validators on 915the action an exception is thrown. $autorun is used internally and 916is set to 1 if the action was executed using autorun. 917 918After the action has been successfully executed and the workflow saved 919we issue a 'execute' observation with the old state, action name and 920an autorun flag as additional parameters. 921So if you wanted to write an observer you could create a 922method with the signature: 923 924 sub update { 925 my ( $class, $workflow, $action, $old_state, $action_name, $autorun ) 926 = @_; 927 if ( $action eq 'execute' ) { .... } 928 } 929 930We also issue a 'change state' observation if the executed action 931resulted in a new state. See L<WORKFLOWS ARE OBSERVABLE> above for how 932we use and register observers. 933 934Returns: new state of workflow 935 936=head3 get_current_actions( $group ) 937 938Returns a list of action names available from the current state for 939the given environment. So if you keep your C<context()> the same if 940you call C<execute_action()> with one of the action names you should 941not trigger any condition error since the action has already been 942screened for conditions. 943If you want to divide actions in groups (for example state change group, 944approval group, which have to be shown at different places on the page) add group property 945to your action 946 947<action name="terminate request" group="state change" class="MyApp::Action::Terminate" /> 948<action name="approve request" group="approval" class="MyApp::Action::Approve" /> 949 950my @actions = $wf->get_current_actions("approval"); 951 952$group should be string that reperesents desired group name. In @actions you will get 953list of action names available from the current state for the given environment limited by group. 954$group is optional parameter. 955 956Returns: list of strings representing available actions 957 958=head3 get_action( $action_name ) 959 960Retrieves the action object associated with C<$action_name> in the 961current workflow state. This will throw an exception if: 962 963=over 4 964 965=item * 966 967No workflow state exists with a name of the current state. (This is 968usually some sort of configuration error and should be caught at 969initialization time, so it should not happen.) 970 971=item * 972 973No action C<$action_name> exists in the current state. 974 975=item * 976 977No action C<$action_name> exists in the workflow universe. 978 979=item * 980 981One of the conditions for the action in this state is not met. 982 983=back 984 985 986=head3 get_action_fields( $action_name ) 987 988Return a list of L<Workflow::Action::InputField> objects for the given 989C<$action_name>. If C<$action_name> not in the current state or not 990accessible by the environment an exception is thrown. 991 992Returns: list of L<Workflow::Action::InputField> objects 993 994=head3 add_history( @( \%params | $wf_history_object ) ) 995 996Adds any number of histories to the workflow, typically done by an 997action in C<execute_action()> or one of the observers of that 998action. This history will not be saved until C<execute_action()> is 999complete. 1000 1001You can add a list of either hashrefs with history information in them 1002or full L<Workflow::History> objects. Trying to add anything else will 1003result in an exception and B<none> of the items being added. 1004 1005Successfully adding the history objects results in a 'add history' 1006observation being thrown. See L<WORKFLOWS ARE OBSERVABLE> above for 1007more. 1008 1009Returns: nothing 1010 1011=head3 get_history() 1012 1013Returns list of history objects for this workflow. Note that some may 1014be unsaved if you call this during the C<execute_action()> process. 1015 1016=head3 get_unsaved_history() 1017 1018Returns list of all unsaved history objects for this workflow. 1019 1020=head3 clear_history() 1021 1022Clears all transient history objects from the workflow object, B<not> 1023from the long-term storage. 1024 1025=head3 set( $property, $value ) 1026 1027Method used to overwrite L<Class::Accessor> so only certain callers can set 1028properties caller has to be a L<Workflow> namespace package. 1029 1030Sets property to value or throws L<Workflow::Exception> 1031 1032=head2 Properties 1033 1034Unless otherwise noted, properties are B<read-only>. 1035 1036=head3 Configuration Properties 1037 1038Some properties are set in the configuration file for each 1039workflow. These remain static once the workflow is instantiated. 1040 1041B<type> 1042 1043Type of workflow this is. You may have many individual workflows 1044associated with a type or you may have many different types 1045running in a single workflow engine. 1046 1047B<description> 1048 1049Description (usually brief, hopefully with a URL...) of this 1050workflow. 1051 1052B<time_zone> 1053 1054Workflow uses the DateTime module to create all date objects. The time_zone 1055parameter allows you to pass a time zone value directly to the DateTime 1056new method for all cases where Workflow needs to create a date object. 1057See the DateTime module for acceptable values. 1058 1059=head3 Dynamic Properties 1060 1061You can get the following properties from any workflow object. 1062 1063B<id> 1064 1065ID of this workflow. This will B<always> be defined, since when the 1066L<Workflow::Factory> creates a new workflow it first saves it to 1067long-term storage. 1068 1069B<state> 1070 1071The current state of the workflow. 1072 1073B<last_update> (read-write) 1074 1075Date of the workflow's last update. 1076 1077=head3 context (read-write, see below) 1078 1079A L<Workflow::Context> object associated with this workflow. This 1080should never be undefined as the L<Workflow::Factory> sets an empty 1081context into the workflow when it is instantiated. 1082 1083If you add a context to a workflow and one already exists, the values 1084from the new workflow will overwrite values in the existing 1085workflow. This is a shallow merge, so with the following: 1086 1087 $wf->context->param( drinks => [ 'coke', 'pepsi' ] ); 1088 my $context = Workflow::Context->new(); 1089 $context->param( drinks => [ 'beer', 'wine' ] ); 1090 $wf->context( $context ); 1091 print 'Current drinks: ', join( ', ', @{ $wf->context->param( 'drinks' ) } ); 1092 1093You will see: 1094 1095 Current drinks: beer, wine 1096 1097=head2 Internal Methods 1098 1099=head3 init( $id, $current_state, \%workflow_config, \@wf_states ) 1100 1101B<THIS SHOULD ONLY BE CALLED BY THE> L<Workflow::Factory>. Do not call 1102this or the C<new()> method yourself -- you will only get an 1103exception. Your only interface for creating and fetching workflows is 1104through the factory. 1105 1106This is called by the inherited constructor and sets the 1107C<$current_state> value to the property C<state> and uses the other 1108non-state values from C<\%config> to set parameters via the inherited 1109C<param()>. 1110 1111=head3 _get_workflow_state( [ $state ] ) 1112 1113Return the L<Workflow::State> object corresponding to C<$state>, which 1114defaults to the current state. 1115 1116=head3 _set_workflow_state( $wf_state ) 1117 1118Assign the L<Workflow::State> object C<$wf_state> to the workflow. 1119 1120=head3 _get_next_state( $action_name ) 1121 1122Returns the name of the next state given the action 1123C<$action_name>. Throws an exception if C<$action_name> not contained 1124in the current state. 1125 1126=head3 add_observer( @observers ) 1127 1128Adds one or more observers to a C<Workflow> instance. An observer is a 1129function. See L</notify_observers> for its calling convention. 1130 1131This function is used internally by C<Workflow::Factory> to implement 1132observability as documented in the section L</WORKFLOWS ARE OBSERVABLE> 1133 1134=head3 notify_observers( @arguments ) 1135 1136Calls all observer functions registered through C<add_observer> with 1137the workflow as the first argument and C<@arguments> as the remaining 1138arguments: 1139 1140 $observer->( $wf, @arguments ); 1141 1142Used by various parts of the library to notify observers of workflow 1143instance related events. 1144 1145 1146=head1 CONFIGURATION AND ENVIRONMENT 1147 1148The configuration of Workflow is done using the format of your choice, currently 1149XML and Perl is implemented, but additional formats can be added, please refer 1150to L<Workflow::Config>, for implementation details. 1151 1152=head1 DEPENDENCIES 1153 1154=over 1155 1156=item L<Class::Accessor> 1157 1158=item L<Class::Factory> 1159 1160=item L<DateTime> 1161 1162=item L<DateTime::Format::Strptime> 1163 1164=item L<Exception::Class> 1165 1166=item L<Log::Log4perl> 1167 1168=item L<Safe> 1169 1170=item L<XML::Simple> 1171 1172=item L<DBI> 1173 1174=item L<Data::Dumper> 1175 1176=item L<Carp> 1177 1178=item L<File::Slurp> 1179 1180=item L<Data::UUID> 1181 1182=back 1183 1184=head2 DEPENDENCIES FOR THE EXAMPLE APPLICATION 1185 1186=over 1187 1188=item L<CGI> 1189 1190=item L<CGI::Cookie> 1191 1192=item L<DBD::SQLite> 1193 1194=item L<HTTP::Daemon> 1195 1196=item L<HTTP::Request> 1197 1198=item L<HTTP::Response> 1199 1200=item L<HTTP::Status> 1201 1202=item L<Template> (Template Toolkit) 1203 1204=back 1205 1206For Win32 systems you can get the Template Toolkit and DBD::SQLite 1207PPDs from TheoryX: 1208 1209=over 1210 1211=item * L<http://theoryx5.uwinnipeg.ca/cgi-bin/ppmserver?urn:/PPMServer58> 1212 1213=back 1214 1215=head1 INCOMPATIBILITIES 1216 1217=head2 XML::Simple 1218 1219CPAN testers reports however do demonstrate a problem with one of the 1220dependencies of Workflow, namely L<XML::Simple>. 1221 1222The L<XML::Simple> makes use of L<Lib::XML::SAX> or L<XML::Parser>, the default. 1223 1224In addition an L<XML::Parser> can makes use of plugin parser and some of these 1225might not be able to parse the XML utilized in Workflow. The problem have been 1226observed with L<XML::SAX::RTF>. 1227 1228The following diagnostic points to the problem: 1229 1230 No _parse_* routine defined on this driver (If it is a filter, remember to 1231 set the Parent property. If you call the parse() method, make sure to set a 1232 Source. You may want to call parse_uri, parse_string or parse_file instead.) 1233 1234Your L<XML::SAX> configuration is located in the file: 1235 1236 XML/SAX/ParserDetails.ini 1237 1238=head1 BUGS AND LIMITATIONS 1239 1240Known bugs and limitations can be seen in the Github issue tracker: 1241 1242L<https://github.com/jonasbn/perl-workflow/issues> 1243 1244=head1 BUG REPORTING 1245 1246Bug reporting should be done either via Github issues 1247 1248L<https://github.com/jonasbn/perl-workflow/issues> 1249 1250A list of currently known issues can be seen via the same URL. 1251 1252=head1 TEST 1253 1254The test suite can be run using, L<Module::Build> 1255 1256 % ./Build test 1257 1258Some of the tests are reserved for the developers and are only run of the 1259environment variable C<TEST_AUTHOR> is set to true. 1260 1261=head1 TEST COVERAGE 1262 1263This is the current test coverage of Workflow version 1.58, with the C<TEST_AUTHOR> 1264flag enabled 1265 1266 TEST_AUTHOR=1 dzil cover 1267 1268 ---------------------------- ------ ------ ------ ------ ------ ------ ------ 1269 File stmt bran cond sub pod time total 1270 ---------------------------- ------ ------ ------ ------ ------ ------ ------ 1271 blib/lib/Workflow.pm 91.6 68.7 60.0 93.3 100.0 1.2 86.7 1272 blib/lib/Workflow/Action.pm 93.5 60.0 n/a 94.1 100.0 4.4 91.4 1273 ...b/Workflow/Action/Null.pm 100.0 n/a n/a 100.0 100.0 2.3 100.0 1274 blib/lib/Workflow/Base.pm 96.7 86.3 83.3 100.0 100.0 3.0 94.5 1275 ...lib/Workflow/Condition.pm 100.0 100.0 100.0 100.0 100.0 4.4 100.0 1276 .../Condition/CheckReturn.pm 71.7 35.7 n/a 100.0 100.0 0.0 67.6 1277 ...low/Condition/Evaluate.pm 96.7 75.0 n/a 100.0 100.0 3.4 95.4 1278 ...low/Condition/GreedyOR.pm 100.0 100.0 n/a 100.0 100.0 0.0 100.0 1279 ...flow/Condition/HasUser.pm 70.0 n/a 33.3 83.3 100.0 0.0 70.0 1280 ...flow/Condition/LazyAND.pm 100.0 100.0 n/a 100.0 100.0 0.0 100.0 1281 ...kflow/Condition/LazyOR.pm 100.0 100.0 n/a 100.0 100.0 0.0 100.0 1282 ...flow/Condition/Negated.pm 100.0 n/a n/a 100.0 100.0 0.0 100.0 1283 blib/lib/Workflow/Config.pm 96.2 81.2 33.3 100.0 100.0 3.1 92.2 1284 ...b/Workflow/Config/Perl.pm 96.1 83.3 66.6 92.8 100.0 0.1 92.9 1285 ...ib/Workflow/Config/XML.pm 94.1 62.5 50.0 100.0 100.0 4.6 90.2 1286 blib/lib/Workflow/Context.pm 100.0 n/a n/a 100.0 100.0 2.3 100.0 1287 ...lib/Workflow/Exception.pm 100.0 100.0 n/a 100.0 100.0 0.9 100.0 1288 blib/lib/Workflow/Factory.pm 87.4 79.3 61.5 84.6 100.0 30.9 84.3 1289 blib/lib/Workflow/History.pm 100.0 87.5 n/a 100.0 100.0 4.3 98.2 1290 ...ib/Workflow/InputField.pm 98.6 96.1 87.5 100.0 100.0 2.5 97.6 1291 ...lib/Workflow/Persister.pm 98.4 100.0 71.4 94.7 100.0 2.4 96.4 1292 ...Workflow/Persister/DBI.pm 86.7 72.0 35.2 90.6 100.0 7.7 83.0 1293 ...er/DBI/AutoGeneratedId.pm 91.8 75.0 83.3 100.0 100.0 0.0 88.7 1294 ...ersister/DBI/ExtraData.pm 29.8 0.0 0.0 60.0 100.0 0.6 29.7 1295 ...rsister/DBI/SequenceId.pm 100.0 n/a 50.0 100.0 100.0 0.0 98.0 1296 ...orkflow/Persister/File.pm 94.4 50.0 33.3 100.0 100.0 0.2 88.5 1297 ...low/Persister/RandomId.pm 100.0 n/a 100.0 100.0 100.0 2.3 100.0 1298 ...orkflow/Persister/UUID.pm 100.0 n/a n/a 100.0 100.0 2.2 100.0 1299 blib/lib/Workflow/State.pm 88.1 62.5 16.6 96.3 100.0 4.9 81.7 1300 ...lib/Workflow/Validator.pm 100.0 83.3 n/a 100.0 100.0 2.4 97.5 1301 ...dator/HasRequiredField.pm 90.9 50.0 n/a 100.0 100.0 2.3 87.8 1302 ...dator/InEnumeratedType.pm 100.0 100.0 n/a 100.0 100.0 2.3 100.0 1303 ...ator/MatchesDateFormat.pm 100.0 100.0 100.0 100.0 100.0 4.0 100.0 1304 Total 90.7 73.6 57.6 94.9 100.0 100.0 87.8 1305 ---------------------------- ------ ------ ------ ------ ------ ------ ------ 1306 1307Activities to get improved coverage are ongoing. 1308 1309=head1 QUALITY ASSURANCE 1310 1311The Workflow project utilizes L<Perl::Critic> in an attempt to avoid common 1312pitfalls and programming mistakes. 1313 1314The static analysis performed by L<Perl::Critic> is integrated into the L</TEST> 1315tool chain and is performed either by running the test suite. 1316 1317 % ./Build test 1318 1319Or by running the test file containing the L<Perl::Critic> tests explicitly. 1320 1321 % ./Build test --verbose 1 --test_files t/04_critic.t 1322 1323Or 1324 1325 % perl t/critic.t 1326 1327The test does however require that the TEST_AUTHOR flag is set since this is 1328regarded as a part of the developer tool chain and we do not want to disturb 1329users and CPAN testers with this. 1330 1331The following policies are disabled 1332 1333=over 1334 1335=item * L<Perl::Critic::Policy::ValuesAndExpressions::ProhibitMagicNumbers> 1336 1337=item * L<Perl::Critic::Policy::Subroutines::ProhibitExplicitReturnUndef> 1338 1339=item * L<Perl::Critic::Policy::NamingConventions::ProhibitAmbiguousNames> 1340 1341=item * L<Perl::Critic::Policy::ValuesAndExpressions::ProhibitConstantPragma> 1342 1343=back 1344 1345The complete policy configuration can be found in t/perlcriticrc. 1346 1347Currently a large number other policies are disabled, but these are being 1348addressed as ongoing work and they will either be listed here or changes will 1349be applied, which will address the Workflow code's problematic areas from 1350L<Perl::Critic> perspective. 1351 1352=head1 CODING STYLE 1353 1354Currently the code is formatted using L<Perl::Tidy>. The resource file can be 1355downloaded from the central repository. 1356 1357 notes/perltidyrc 1358 1359=head1 PROJECT 1360 1361The Workflow project is currently hosted on GitHub 1362 1363=over 1364 1365=item GitHub: L<https://github.com/jonasbn/perl-workflow> 1366 1367=back 1368 1369=head2 REPOSITORY 1370 1371The code is kept under revision control using Git: 1372 1373=over 1374 1375=item L<https://github.com/jonasbn/perl-workflow/tree/master/> 1376 1377=back 1378 1379=head2 OTHER RESOURCES 1380 1381=over 1382 1383=item * CPAN Ratings 1384 1385L<http://cpanratings.perl.org/d/Workflow> 1386 1387=item * MetaCPAN 1388 1389L<https://metacpan.org/release/Workflow> 1390 1391=back 1392 1393=head1 SEE ALSO 1394 1395=over 1396 1397=item * November 2010 talk 'Workflow' given at Nordic Perl Workshop 2010 in Reykjavik, Iceland by jonasbn 1398L<http://www.slideshare.net/jonasbn/workflow-npw2010> 1399 1400=item * August 2010 talk 'Workflow' given at YAPC::Europe 2010 in Pisa, Italy by jonasbn 1401L<http://www.slideshare.net/jonasbn/workflow-yapceu2010> 1402 1403=back 1404 1405=head1 COPYRIGHT 1406 1407Copyright (c) 2003 Chris Winters and Arvato Direct; 1408Copyright (c) 2004-2022 Chris Winters. All rights reserved. 1409 1410This library is free software; you can redistribute it and/or modify 1411it under the same terms as Perl itself. 1412 1413=head1 AUTHORS 1414 1415=encoding utf8 1416 1417Jonas B. (jonasbn) E<lt>jonasbn@cpan.orgE<gt>, current maintainer. 1418 1419Chris Winters E<lt>chris@cwinters.comE<gt>, original author. 1420 1421The following folks have also helped out (listed here in no particular order): 1422 1423Thanks for to Michiel W. Beijen for fix to badly formatted URL, included in release 1.52 1424 1425Several PRs (13 to be exact) from Erik Huelsmann resulting in release 1.49. Yet another 1426batch of PRs resulted in release 1.50 1427 1428PR from Mohammad S Anwar correcting some POD errors, included in release 1.49 1429 1430Bug report from Petr Pisar resulted in release 1.48 1431 1432Bug report from Tina Müller (tinita) resulted in release 1.47 1433 1434Bug report from Slaven Rezić resulting in maintenance release 1.45 1435 1436Feature and bug fix by dtikhonov resulting in 1.40 (first pull request on Github) 1437 1438Sérgio Alves, patch to timezone handling for workflow history deserialized using 1439DBI persister resulting in 1.38 1440 1441Heiko Schlittermann for context serialization patch resulting in 1.36 1442 1443Scott Harding, for lazy evaluation of conditions and for nested conditions, see 1444Changes file: 1.35 1445 1446Oliver Welter, patch implementing custom workflows, see Changes file: 1.35 and 1447patch related to this in 1.37 and factory subclassing also in 1.35. Improvements 1448in logging for condition validation in 1.43 and 1.44 and again a patch resulting 1449in release 1.46 1450 1451Steven van der Vegt, patch for autorun in initial state and improved exception 1452handling for validators, see Changes file: 1.34_1 1453 1454Andrew O'Brien, patch implementing dynamic reloaded of flows, see Changes file: 14551.33 1456 1457Sergei Vyshenski, bug reports - addressed and included in 1.33, Sergei also 1458maintains the FreeBSD port 1459 1460Alejandro Imass, improvements and clarifications, see Changes file: 1.33 1461 1462Danny Sadinoff, patches to give better control of initial state and history 1463records for workflow, see Changes file: 1.33 1464 1465Thomas Erskine, for patch adding new accessors and fixing several bugs see 1466Changes file 1.33 1467 1468Ivan Paponov, for patch implementing action groups, see Changes file, 1.33 1469 1470Robert Stockdale, for patch implementing dynamic names for conditions, see 1471Changes file, 1.32 1472 1473Jim Brandt, for patch to Workflow::Config::XML. See Changes file, 0.27 and 0.30 1474 1475Alexander Klink, for: patches resulting in 0.23, 0.24, 0.25, 0.26 and 0.27 1476 1477Michael Bell, for patch resulting in 0.22 1478 1479Martin Bartosch, for bug reporting and giving the solution not even using a 1480patch (0.19 to 0.20) and a patch resulting in 0.21 1481 1482Randal Schwartz, for testing 0.18 and swiftly giving feedback (0.18 to 0.19) 1483 1484Chris Brown, for a patch to L<Workflow::Config::Perl> (0.17 to 0.18) 1485 1486Dietmar Hanisch E<lt>Dietmar.Hanisch@Bertelsmann.deE<gt> - Provided 1487most of the good ideas for the module and an excellent example of 1488everyday use. 1489 1490Tom Moertel E<lt>tmoertel@cpan.orgE<gt> gave me the idea for being 1491able to attach event listeners (observers) to the process. 1492 1493Michael Roberts E<lt>michael@vivtek.comE<gt> graciously released the 1494'Workflow' namespace on CPAN; check out his Workflow toolkit at 1495L<http://www.vivtek.com/wftk.html>. 1496 1497Michael Schwern E<lt>schwern@pobox.orgE<gt> barked via RT about a 1498dependency problem and CPAN naming issue. 1499 1500Jim Smith E<lt>jgsmith@tamu.eduE<gt> - Contributed patches (being able 1501to subclass L<Workflow::Factory>) and good ideas. 1502 1503Martin Winkler E<lt>mw@arsnavigandi.deE<gt> - Pointed out a bug and a 1504few other items. 1505 1506=cut 1507