File Coverage

blib/lib/Workflow.pm
Criterion Covered Total %
statement 169 188 89.8
branch 34 48 70.8
condition 14 21 66.6
subroutine 30 33 90.9
pod 15 15 100.0
total 262 305 85.9


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