File Coverage

lib/Workflow.pm
Criterion Covered Total %
statement 166 192 86.4
branch 30 48 62.5
condition 10 15 66.6
subroutine 28 30 93.3
pod 13 13 100.0
total 247 298 82.8


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