-
Notifications
You must be signed in to change notification settings - Fork 11
/
Workflow.pm
1506 lines (1046 loc) · 45.7 KB
/
Workflow.pm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package Workflow;
use warnings;
use strict;
use v5.14.0; # warnings
use base qw( Workflow::Base );
use Log::Log4perl qw( get_logger );
use Workflow::Context;
use Workflow::Exception qw( workflow_error );
use Exception::Class;
use Workflow::Factory qw( FACTORY );
use Carp qw(croak carp);
use English qw( -no_match_vars );
my @FIELDS = qw( id type description state last_update time_zone );
my @INTERNAL = qw( _factory _observers );
__PACKAGE__->mk_accessors( @FIELDS, @INTERNAL );
$Workflow::VERSION = '1.59';
use constant NO_CHANGE_VALUE => 'NOCHANGE';
########################################
# INTERNAL METHODS
sub add_observer {
my ($self, @observers) = @_;
if (not $self->_observers) {
$self->_observers( [] );
}
push @{$self->_observers}, @observers;
return;
}
sub notify_observers {
my ($self, @args) = @_;
return unless $self->_observers;
$_->($self, @args) for @{$self->_observers};
return;
}
########################################
# PUBLIC METHODS
# this is our only read-write property...
sub context {
my ( $self, $context ) = @_;
if ($context) {
# We already have a context, merge the new one with ours; (the
# new one wins with dupes)
if ( $self->{context} ) {
$self->{context}->merge($context);
} else {
$context->param( workflow_id => $self->id );
$self->{context} = $context;
}
}
unless ( $self->{context} ) {
$self->{context} = Workflow::Context->new();
}
return $self->{context};
}
sub get_current_actions {
my ( $self, $group ) = @_;
$self->log->debug( "Getting current actions for wf '", $self->id, "'" );
my $wf_state = $self->_get_workflow_state;
return $wf_state->get_available_action_names( $self, $group );
}
sub get_action {
my ( $self, $action_name ) = @_;
my $state = $self->state;
$self->log->debug(
"Trying to find action '$action_name' in state '$state'");
my $wf_state = $self->_get_workflow_state;
unless ( $wf_state->contains_action($action_name) ) {
workflow_error
"State '$state' does not contain action '$action_name'";
}
$self->log->debug("Action '$action_name' exists in state '$state'");
my $action = $self->_get_workflow_state()->get_action( $self, $action_name );
# This will throw an exception which we want to bubble up
$wf_state->evaluate_action( $self, $action_name );
return $action;
}
sub get_action_fields {
my ( $self, $action_name ) = @_;
my $action = $self->get_action($action_name);
return $action->fields;
}
sub execute_action {
my ( $self, $action_name, $autorun ) = @_;
# This checks the conditions behind the scenes, so there's no
# explicit 'check conditions' step here
my $action = $self->get_action($action_name);
# Need this in case we encounter an exception after we store the
# new state
my $old_state = $self->state;
my ( $new_state, $action_return );
eval {
$action->validate($self);
$self->log->debug("Action validated ok");
$action_return = $action->execute($self);
$self->log->debug("Action executed ok");
$new_state = $self->_get_next_state( $action_name, $action_return );
if ( $new_state ne NO_CHANGE_VALUE ) {
$self->log->info("Set new state '$new_state' after action executed");
$self->state($new_state);
}
# this will save the workflow histories as well as modify the
# state of the workflow history to reflect the NEW state of
# the workflow; if it fails we should have some means for the
# factory to rollback other transactions...
# Update
# Jim Brandt 4/16/2008: Implemented transactions for DBI persisters.
# Implementation still depends on each persister.
$self->_factory()->save_workflow($self);
# If using a DBI persister with no autocommit, commit here.
$self->_factory()->_commit_transaction($self);
$self->log->info("Saved workflow with possible new state ok");
};
# If there's an exception, reset the state to the original one and
# rethrow
if ($EVAL_ERROR) {
my $error = $EVAL_ERROR;
$self->log->error(
"Caught exception from action: $error; reset ",
"workflow to old state '$old_state'"
);
$self->state($old_state);
$self->_factory()->_rollback_transaction($self);
# If it is a validation error we rethrow it so it can be evaluated
# by the caller to provide better feedback to the user
if (Exception::Class->caught('Workflow::Exception::Validation')) {
$EVAL_ERROR->rethrow();
}
# Don't use 'workflow_error' here since $error should already
# be a Workflow::Exception object or subclass
croak $error;
}
# clear condition cache on state change
delete $self->{'_condition_result_cache'};
$self->notify_observers( 'execute', $old_state, $action_name, $autorun );
my $new_state_obj = $self->_get_workflow_state;
if ( $old_state ne $new_state ) {
$self->notify_observers( 'state change', $old_state, $action_name,
$autorun );
}
if ( $new_state_obj->autorun ) {
$self->log->info(
"State '$new_state' marked to be run ",
"automatically; executing that state/action..."
);
$self->_auto_execute_state($new_state_obj);
}
return $self->state;
}
sub add_history {
my ( $self, @items ) = @_;
my @to_add = ();
foreach my $item (@items) {
if ( ref $item eq 'HASH' ) {
$item->{workflow_id} = $self->id;
$item->{time_zone} = $self->time_zone();
push @to_add, Workflow::History->new($item);
$self->log->debug("Adding history from hashref");
} elsif ( ref $item and $item->isa('Workflow::History') ) {
$item->workflow_id( $self->id );
push @to_add, $item;
$self->log->debug("Adding history object directly");
} else {
workflow_error "I don't know how to add a history of ", "type '",
ref($item), "'";
}
if ($EVAL_ERROR) {
workflow_error "Unable to assert history object";
}
}
push @{ $self->{_histories} }, @to_add;
$self->notify_observers( 'add history', \@to_add );
}
sub get_history {
my ($self) = @_;
$self->{_histories} ||= [];
my @uniq_history = ();
my %seen_ids = ();
my @all_history = (
$self->_factory()->get_workflow_history($self),
@{ $self->{_histories} }
);
foreach my $history (@all_history) {
my $id = $history->id;
if ($id) {
unless ( $seen_ids{$id} ) {
push @uniq_history, $history;
}
$seen_ids{$id}++;
} else {
push @uniq_history, $history;
}
}
return @uniq_history;
}
sub get_unsaved_history {
my ($self) = @_;
return grep { !$_->is_saved } @{ $self->{_histories} };
}
sub clear_history {
my ($self) = @_;
$self->{_histories} = [];
}
########################################
# PRIVATE METHODS
sub init {
my ( $self, $id, $current_state, $config, $wf_state_objects, $factory )
= @_;
$id ||= '';
$factory ||= FACTORY;
$self->log->info(
"Instantiating workflow of with ID '$id' and type ",
"'$config->{type}' with current state '$current_state'"
);
$self->id($id) if ($id);
$self->_factory($factory);
$self->state($current_state);
$self->type( $config->{type} );
$self->description( $config->{description} );
my $time_zone
= exists $config->{time_zone} ? $config->{time_zone} : 'floating';
$self->time_zone($time_zone);
# other properties go into 'param'...
while ( my ( $key, $value ) = each %{$config} ) {
next if ( $key =~ /^(type|description)$/ );
next if ( ref $value );
$self->log->debug("Assigning parameter '$key' -> '$value'");
$self->param( $key, $value );
}
# Now set all the Workflow::State objects created and cached by the
# factory
foreach my $wf_state ( @{$wf_state_objects} ) {
$self->_set_workflow_state($wf_state);
}
}
# Override from Class::Accessor so only certain callers can set
# properties
sub set {
my ( $self, $prop, $value ) = @_;
my $calling_pkg = ( caller 1 )[0];
unless ( $calling_pkg =~ /^Workflow/ ) {
carp "Tried to set from: ", join ', ', caller 1;
workflow_error
"Don't try to use my private setters from '$calling_pkg'!";
}
$self->{$prop} = $value;
}
sub _get_action { # for backward compatibility with 1.49 and before
goto &get_action;
}
sub _get_workflow_state {
my ( $self, $state ) = @_;
$state ||= ''; # get rid of -w...
my $use_state = $state || $self->state;
$self->log->debug(
"Finding Workflow::State object for state [given: $use_state] ",
"[internal: ", $self->state, "]" );
my $wf_state = $self->{_states}{$use_state};
unless ($wf_state) {
workflow_error "No state '$use_state' exists in workflow '",
$self->type, "'";
}
return $wf_state;
}
sub _set_workflow_state {
my ( $self, $wf_state ) = @_;
$self->{_states}{ $wf_state->state } = $wf_state;
}
sub _get_next_state {
my ( $self, $action_name, $action_return ) = @_;
my $wf_state = $self->_get_workflow_state;
return $wf_state->get_next_state( $action_name, $action_return );
}
sub _auto_execute_state {
my ( $self, $wf_state ) = @_;
my $action_name;
eval { $action_name = $wf_state->get_autorun_action_name($self); };
if ($EVAL_ERROR)
{ # we found an error, possibly more than one or none action
# are available in this state
if ( !$wf_state->may_stop() ) {
# we are in autorun, but stopping is not allowed, so
# rethrow
my $error = $EVAL_ERROR;
$error->rethrow();
}
} else { # everything is fine, execute action
$self->log->debug(
"Found action '$action_name' to execute in ",
"autorun state ",
$wf_state->state
);
$self->execute_action( $action_name, 1 );
}
}
1;
__END__
=pod
=begin markdown
[![CPAN version](https://badge.fury.io/pl/Workflow.svg)](http://badge.fury.io/pl/Workflow)
[![Build Status](https://travis-ci.org/jonasbn/perl-workflow.svg?branch=master)](https://travis-ci.org/jonasbn/perl-workflow)
[![Coverage Status](https://coveralls.io/repos/github/jonasbn/perl-workflow/badge.svg?branch=master)](https://coveralls.io/github/jonasbn/perl-workflow?branch=master)
=end markdown
=head1 NAME
Workflow - Simple, flexible system to implement workflows
=head1 VERSION
This documentation describes version 1.59 of Workflow
=head1 SYNOPSIS
use Workflow::Factory qw( FACTORY );
# Defines a workflow of type 'myworkflow'
my $workflow_conf = 'workflow.xml';
# contents of 'workflow.xml'
<workflow>
<type>myworkflow</type>
<time_zone>local</time_zone>
<description>This is my workflow.</description>
<state name="INITIAL">
<action name="upload file" resulting_state="uploaded" />
</state>
<state name="uploaded" autorun="yes">
<action name="verify file" resulting_state="verified file">
<!-- everyone other than 'CWINTERS' must verify -->
<condition test="$context->{user} ne 'CWINTERS'" />
</action>
<action name="null" resulting_state="annotated">
<condition test="$context->{user} eq 'CWINTERS'" />
</action>
</state>
<state name="verified file">
<action name="annotate">
<condition name="can_annotate" />
</action>
<action name="null">
<condition name="!can_annotate" />
</action>
</state>
<state name="annotated" autorun="yes" may_stop="yes">
<action name="null" resulting_state="finished">
<condition name="completed" />
</action>
</state>
<state name="finished" />
</workflow>
# Defines actions available to the workflow
my $action_conf = 'action.xml';
# contents of 'action.xml'
<actions>
<action name="upload file" class="MyApp::Action::Upload">
<field name="path" label="File Path"
description="Path to file" is_required="yes" />
</action>
<action name="verify file" class="MyApp::Action::Verify">
<validator name="filesize_cap">
<arg>$file_size</arg>
</validator>
</action>
<action name="annotate" class="MyApp::Action::Annotate" />
<action name="null" class="Workflow::Action::Null" />
</actions>
# Defines conditions available to the workflow
my $condition_conf = 'condition.xml';
# contents of 'condition.xml'
<conditions>
<condition name="can_annotate"
class="MyApp::Condition::CanAnnotate" />
</conditions>
# Defines validators available to the actions
my $validator_conf = 'validator.xml';
# contents of 'validator.xml'
<validators>
<validator name="filesize_cap" class="MyApp::Validator::FileSizeCap">
<param name="max_size" value="20M" />
</validator>
</validators>
# Stock the factory with the configurations; we can add more later if
# we want
$self->_factory()->add_config_from_file(
workflow => $workflow_conf,
action => $action_conf,
condition => $condition_conf,
validator => $validator_conf
);
# Instantiate a new workflow...
my $workflow = $self->_factory()->create_workflow( 'myworkflow' );
print "Workflow ", $workflow->id, " ",
"currently at state ", $workflow->state, "\n";
# Display available actions...
print "Available actions: ", $workflow->get_current_actions, "\n";
# Get the data needed for action 'upload file' (assumed to be
# available in the current state) and display the fieldname and
# description
print "Action 'upload file' requires the following fields:\n";
foreach my $field ( $workflow->get_action_fields( 'FOO' ) ) {
print $field->name, ": ", $field->description,
"(Required? ", $field->is_required, ")\n";
}
# Add data to the workflow context for the validators, conditions and
# actions to work with
my $context = $workflow->context;
$context->param( current_user => $user );
$context->param( sections => \@sections );
$context->param( path => $path_to_file );
# Execute one of them
$workflow->execute_action( 'upload file' );
print "New state: ", $workflow->state, "\n";
# Later.... fetch an existing workflow
my $id = get_workflow_id_from_user( ... );
my $workflow = $self->_factory()->fetch_workflow( 'myworkflow', $id );
print "Current state: ", $workflow->state, "\n";
=head1 QUICK START
The F<eg/ticket/> directory contains a configured workflow system.
You can access the same data and logic in two ways:
=over
=item * a command-line application (ticket.pl)
=item * a CGI script (ticket.cgi)
=item * a web application (ticket_web.pl)
=back
To initialize:
perl ticket.pl --db
To run the command-line application:
perl ticket.pl
To access the database and data from CGI, add the relevant
configuration for your web server and call ticket.cgi:
http://www.mysite.com/workflow/ticket.cgi
To start up the standalone web server:
perl ticket_web.pl
(Barring changes to HTTP::Daemon and forking the standalone server
won't work on Win32; use CGI instead, although patches are always
welcome.)
For more info, see F<eg/ticket/README>
=head1 DESCRIPTION
=head2 Overview
This is a standalone workflow system. It is designed to fit into your
system rather than force your system to fit to it. You can save
workflow information to a database or the filesystem (or a custom
storage). The different components of a workflow system can be
included separately as libraries to allow for maximum reusibility.
=head2 User Point of View
As a user you only see two components, plus a third which is really
embedded into another:
=over 4
=item *
L<Workflow::Factory> - The factory is your interface for creating new
workflows and fetching existing ones. You also feed all the necessary
configuration files and/or data structures to the factory to
initialize it.
=item *
L<Workflow> - When you get the workflow object from the workflow
factory you can only use it in a few ways -- asking for the current
state, actions available for the state, data required for a particular
action, and most importantly, executing a particular action. Executing
an action is how you change from one state to another.
=item *
L<Workflow::Context> - This is a blackboard for data from your
application to the workflow system and back again. Each instantiation
of a L<Workflow> has its own context, and actions executed by the
workflow can read data from and deposit data into the context.
=back
=head2 Developer Point of View
The workflow system has four basic components:
=over 4
=item *
B<workflow> - The workflow is a collection of states; you define the
states, how to move from one state to another, and under what
conditions you can change states.
This is represented by the L<Workflow> object. You normally do not
need to subclass this object for customization.
=item *
B<action> - The action is defined by you or in a separate library. The
action is triggered by moving from one state to another and has access
to the workflow and more importantly its context.
The base class for actions is the L<Workflow::Action> class.
=item *
B<condition> - Within the workflow you can attach one or more
conditions to an action. These ensure that actions only get executed
when certain conditions are met. Conditions are completely arbitrary:
typically they will ensure the user has particular access rights, but
you can also specify that an action can only be executed at certain
times of the day, or from certain IP addresses, and so forth. Each
condition is created once at startup then passed a context to check
every time an action is checked to see if it can be executed.
The base class for conditions is the L<Workflow::Condition> class.
=item *
B<validator> - An action can specify one or more validators to ensure
that the data available to the action is correct. The data to check
can be as simple or complicated as you like. Each validator is created
once then passed a context and data to check every time an action is
executed.
The base class for validators is the L<Workflow::Validator> class.
=back
=head1 WORKFLOW BASICS
=head2 Just a Bunch of States
A workflow is just a bunch of states with rules on how to move between
them. These are known as transitions and are triggered by some sort of
event. A state is just a description of object properties. You can
describe a surprisingly large number of processes as a series of
states and actions to move between them. The application shipped with
this distribution uses a fairly common application to illustrate: the
trouble ticket.
When you create a workflow you have one action available to you:
create a new ticket ('create issue'). The workflow has a state
'INITIAL' when it is first created, but this is just a bootstrapping
exercise since the workflow must always be in some state.
The workflow action 'create issue' has a property 'resulting_state',
which just means: if you execute me properly the workflow will be in
the new state 'CREATED'.
All this talk of 'states' and 'transitions' can be confusing, but just
match them to what happens in real life -- you move from one action to
another and at each step ask: what happens next?
You create a trouble ticket: what happens next? Anyone can add
comments to it and attach files to it while administrators can edit it
and developers can start working on it. Adding comments does not
really change what the ticket is, it just adds
information. Attachments are the same, as is the admin editing the
ticket.
But when someone starts work on the ticket, that is a different
matter. When someone starts work they change the answer to: what
happens next? Whenever the answer to that question changes, that means
the workflow has changed state.
=head2 Discover Information from the Workflow
In addition to declaring what the resulting state will be from an
action the action also has a number of 'field' properties that
describe that data it required to properly execute it.
This is an example of discoverability. This workflow system is setup
so you can ask it what you can do next as well as what is required to
move on. So to use our ticket example we can do this, creating the
workflow and asking it what actions we can execute right now:
my $wf = Workflow::$self->_factory()->create_workflow( 'Ticket' );
my @actions = $wf->get_current_actions;
We can also interrogate the workflow about what fields are necessary
to execute a particular action:
print "To execute the action 'create issue' you must provide:\n\n";
my @fields = $wf->get_action_fields( 'create issue' );
foreach my $field ( @fields ) {
print $field->name, " (Required? ", $field->is_required, ")\n",
$field->description, "\n\n";
}
=head2 Provide Information to the Workflow
To allow the workflow to run into multiple environments we must have a
common way to move data between your application, the workflow and the
code that moves it from one state to another.
Whenever the L<Workflow::Factory> creates a new workflow it associates
the workflow with a L<Workflow::Context> object. The context is what
moves the data from your application to the workflow and the workflow
actions.
For instance, the workflow has no idea what the 'current user' is. Not
only is it unaware from an application standpoint but it does not
presume to know where to get this information. So you need to tell it,
and you do so through the context.
The fact that the workflow system proscribes very little means it can
be used in lots of different applications and interfaces. If a system
is too closely tied to an interface (like the web) then you have to
create some potentially ugly hacks to create a more convenient avenue
for input to your system (such as an e-mail approving a document).
The L<Workflow::Context> object is extremely simple to use -- you ask
a workflow for its context and just get/set parameters on it:
# Get the username from the Apache object
my $username = $r->connection->user;
# ...set it in the context
$wf->context->param( user => $username );
# somewhere else you'll need the username:
$news_object->{created_by} = $wf->context->param( 'user' );
=head2 Controlling What Gets Executed
A typical process for executing an action is:
=over 4
=item *
Get data from the user
=item *
Fetch a workflow
=item *
Set the data from the user to the workflow context
=item *
Execute an action on the context
=back
When you execute the action a number of checks occur. The action needs
to ensure:
=over 4
=item *
The data presented to it are valid -- date formats, etc. This is done
with a validator, more at L<Workflow::Validator>
=item *
The environment meets certain conditions -- user is an administrator,
etc. This is done with a condition, more at L<Workflow::Condition>
=back
Once the action passes these checks and successfully executes we
update the permanent workflow storage with the new state, as long as
the application has declared it.
=head1 WORKFLOWS ARE OBSERVABLE
=head2 Purpose
It's useful to have your workflow generate events so that other parts
of a system can see what's going on and react. For instance, say you
have a new user creation process. You want to email the records of all
users who have a first name of 'Sinead' because you're looking for
your long-lost sister named 'Sinead'. You'd create an observer class
like:
package FindSinead;
sub update {
my ( $class, $wf, $event, $new_state ) = @_;
return unless ( $event eq 'state change' );
return unless ( $new_state eq 'CREATED' );
my $context = $wf->context;
return unless ( $context->param( 'first_name' ) eq 'Sinead' );
my $user = $context->param( 'user' );
my $username = $user->username;
my $email = $user->email;
my $mailer = get_mailer( ... );
$mailer->send( 'foo@bar.com','Found her!',
"We found Sinead under '$username' at '$email' );
}
And then associate it with your workflow:
<workflow>
<type>SomeFlow</type>
<observer class="FindSinead" />
...
Every time you create/fetch a workflow the associated observers are
attached to it.
=head2 Events Generated
You can attach listeners to workflows and catch events at a few points
in the workflow lifecycle; these are the events fired:
=over 4
=item *
B<create> - Issued after a workflow is first created.
No additional parameters.
=item *
B<fetch> - Issued after a workflow is fetched from the persister.
No additional parameters.
=item *
B<save> - Issued after a workflow is successfully saved.
No additional parameters.
=item *
B<execute> - Issued after a workflow is successfully executed and
saved.
Adds the parameters C<$old_state>, C<$action_name> and C<$autorun>.
C<$old_state> includes the state of the workflow before the action
was executed, C<$action_name> is the action name that was executed and
C<$autorun> is set to 1 if the action just executed was started
using autorun.
=item *
B<state change> - Issued after a workflow is successfully executed,
saved and results in a state change. The event will not be fired if
you executed an action that did not result in a state change.
Adds the parameters C<$old_state>, C<$action> and C<$autorun>.
C<$old_state> includes the state of the workflow before the action
was executed, C<$action> is the action name that was executed and
C<$autorun> is set to 1 if the action just executed was autorun.
=item *
B<add history> - Issued after one or more history objects added to a
workflow object.
The additional argument is an arrayref of all L<Workflow::History>
objects added to the workflow. (Note that these will not be persisted
until the workflow is persisted.)
=back
=head2 Configuring
You configure the observers directly in the 'workflow' configuration
item. Each 'observer' may have either a 'class' or 'sub' entry within
it that defines the observer's location.
We load these classes at startup time. So if you specify an observer
that doesn't exist you see the error when the workflow system is
initialized rather than the system tries to use the observer.
For instance, the following defines two observers:
<workflow>
<type>ObservedItem</type>
<description>This is...</description>
<observer class="SomeObserver" />
<observer sub="SomeOtherObserver::Functions::other_sub" />
In the first declaration we specify the class ('SomeObserver') that
will catch observations using its C<update()> method. In the second
we're naming exactly the subroutine ('other_sub()' in the class
'SomeOtherObserver::Functions') that will catch observations.
All configured observers get all events. It's up to each observer to
figure out what it wants to handle.
=head1 WORKFLOW METHODS
The following documentation is for the workflow object itself rather
than the entire system.
=head2 Object Methods
=head3 execute_action( $action_name, $autorun )
Execute the action C<$action_name>. Typically this changes the state
of the workflow. If C<$action_name> is not in the current state, fails
one of the conditions on the action, or fails one of the validators on
the action an exception is thrown. $autorun is used internally and
is set to 1 if the action was executed using autorun.
After the action has been successfully executed and the workflow saved
we issue a 'execute' observation with the old state, action name and
an autorun flag as additional parameters.
So if you wanted to write an observer you could create a
method with the signature:
sub update {
my ( $class, $workflow, $action, $old_state, $action_name, $autorun )
= @_;
if ( $action eq 'execute' ) { .... }
}
We also issue a 'change state' observation if the executed action
resulted in a new state. See L<WORKFLOWS ARE OBSERVABLE> above for how
we use and register observers.
Returns: new state of workflow
=head3 get_current_actions( $group )
Returns a list of action names available from the current state for
the given environment. So if you keep your C<context()> the same if
you call C<execute_action()> with one of the action names you should
not trigger any condition error since the action has already been
screened for conditions.
If you want to divide actions in groups (for example state change group,
approval group, which have to be shown at different places on the page) add group property
to your action
<action name="terminate request" group="state change" class="MyApp::Action::Terminate" />
<action name="approve request" group="approval" class="MyApp::Action::Approve" />
my @actions = $wf->get_current_actions("approval");
$group should be string that reperesents desired group name. In @actions you will get
list of action names available from the current state for the given environment limited by group.
$group is optional parameter.
Returns: list of strings representing available actions
=head3 get_action( $action_name )
Retrieves the action object associated with C<$action_name> in the
current workflow state. This will throw an exception if:
=over 4
=item *
No workflow state exists with a name of the current state. (This is
usually some sort of configuration error and should be caught at
initialization time, so it should not happen.)
=item *
No action C<$action_name> exists in the current state.
=item *
No action C<$action_name> exists in the workflow universe.
=item *
One of the conditions for the action in this state is not met.
=back
=head3 get_action_fields( $action_name )
Return a list of L<Workflow::Action::InputField> objects for the given
C<$action_name>. If C<$action_name> not in the current state or not
accessible by the environment an exception is thrown.
Returns: list of L<Workflow::Action::InputField> objects
=head3 add_history( @( \%params | $wf_history_object ) )
Adds any number of histories to the workflow, typically done by an
action in C<execute_action()> or one of the observers of that
action. This history will not be saved until C<execute_action()> is
complete.