File Coverage

lib/Workflow.pm
Criterion Covered Total %
statement 165 190 86.8
branch 29 46 63.0
condition 10 15 66.6
subroutine 28 30 93.3
pod 13 13 100.0
total 245 294 83.3


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